C#基础知识梳理系列六:抽象类与接口

摘 要

抽象类,是一种特殊的类,可以定义具有实现的方法,也可以定义未实现的方法契约,本身不能被实例化,只能在派生类中进行实例化。接口,对一组方法签名进行统一的命名,只能定义未实现的方法契约,本身也不能被实例化,只能在实现类中进行实例化。二者都可以有部分数据成员(如:属性),它们貌似有着相同的“契约”功能,但对各自的派生类(实现类)又有着不同的要求,那么,到底它们有何异同呢?这一章将从四个方面来讲解它们的相同与不同之处。

第一节 定义

抽象类 不能实例化。抽象类的用途是提供多个派生类可共享的基类的公共定义,是对类进行抽象,可以有实现,也可以不实现。使用关键字abstract进行定义。如下定义一个抽象类:

public abstract class Code_06_03
{
}

再来看一下编译器为它生成的IL:

.class public abstract auto ansi beforefieldinit ConsoleApp.Example06.Code_06_03
       extends [mscorlib]System.Object
{
} // end of class ConsoleApp.Example06.Code_06_03

可以看以,抽象类实际上是继承了System.Object类,并且编译器为它生成了一个默认的构造函数。

接口 它是对一组方法签名进行统一命名,是对一组行为规范的定义,各个行为(方法)之间相互疏。使用关键字interface进行定义,如下定义一个接口:

public interface ICode_06_01
{
}

再来看一下编译器的工作:

.class interface public abstract auto ansi ConsoleApp.Example06.ICode_06_01
{
} // end of class ConsoleApp.Example06.ICode_06_01

可以看到,接口实际上是把它当成抽象类来看待,但是没有构造函数。

无论是抽象类拥有构造函数,还是接口不拥有构造函数,它们都是不能被实例化的。

 

第二节 成员的区别

抽象类:描述

1) 可以定义抽象方法,抽象方法没有具体实现,仅仅是一个方法的契约,在子类中重写该方法。抽象类可以重写父类的虚方法为抽象方法。

2) 可以定义非抽象方法,但要求该方法要有具体实现,如果该方法是虚方法,则在子类中可以重写该方法。

3) 可以定义字段,属性,抽象属性,事件及静态成员。

如下是对类Code_06_03的扩充:

View Code
public abstract class Code_06_03
    {
        Dictionary<Guid, string> Root = new Dictionary<Guid, string>();
        public string Sex { get; set; }
        public abstract string Address { get; }
        public abstract int Add(int a, int b);
        protected virtual string GetAddress(string addressID)
        {
            return addressID + " 北京";
        }

        public void AddRoot(Guid id, string rootName)
        {
            this.Root.Add(id, rootName);
            OnAddRoot();
        }
        public event EventHandler AddRootEvent;
        void OnAddRoot()
        {
            EventHandler handler = AddRootEvent;
            if (handler != null)
            {
                handler(this, null);
            }
        }

        public string this[Guid key]
        {
            get
            {
                return Root[key];
            }
            set
            {
                Root[key] = value;
            }
        }
}

抽象方法public abstract int Add(int a, int b);的IL:

.method public hidebysig newslot abstract virtual 
        instance int32  Add(int32 a,
                            int32 b) cil managed
{
} // end of method Code_06_03::Add

编译器把Add方法当作一个虚方法,在子类中可以被重写。

虚方法protected virtual string GetAddress(string addressID)的IL:

.method family hidebysig newslot virtual 
        instance string  GetAddress(string addressID) cil managed
{
//省略
}

它本来就是一个虚方法,所以编译器并没有特殊对待它。

方法public void AddRoot(Guid id, string rootName)的IL:

.method public hidebysig instance void  AddRoot(valuetype [mscorlib]System.Guid id,
                                                string rootName) cil managed
{
//省略
}

也是一个普通的对象方法。

接口

1) 可以定义属性及索引器,但不能定义字段。

2) 可以定义事件。

3) 可以定义方法,仅仅是方法签名的约定,不得有实现,在实现类中对该方法进行具体实现,有点类似于抽象类的抽象方法。

4) 不可以定义虚方法。

5) 不可以定义任何静态成员。

6) 接口成员默认是全开放的,不得有访问修饰符。

如下,定义一个接口:

    public interface ICode_06_01
    {
        string Name { get; set; }
        int Add(int a, int b);
        event EventHandler AddEvent;
    }

方法int Add(int a, int b);的IL:

.method public hidebysig newslot abstract virtual 
        instance int32  Add(int32 a,
                            int32 b) cil managed
{
} // end of method ICode_06_01::Add

可以看到,定义的时候,我们并没有为其指定可访问修饰符(编译器也不允许我们明文指定其可访问修饰符),但编译器默认将它的访问级别指定为public。另外是把它当作一个抽象的虚方法。

至于成员属性和事件,编译器则将它们当作普通的对象属性和对象事件对待,会为它们生成相应的get/set和add/remove 方法,并无特别之处。

第三节 实现方式的区别

抽象类的实现

由于抽象类也是类,所以对它的实现就像普通的继承一样,子类通过继承可以得到抽象类的公有成员,且可以重写部分成员,如虚方法和抽象方法等。如下是对Code_06_03类的实现:

public class Code_06_04 : Code_06_03
    {
        public override int Add(int a, int b)
        {
            return a + b;
        }
        protected override string GetAddress(string addressID)
        {
            return "BeiJing";
        }
        string _addressPrefix = "China ";
        public override string Address
        {
            get { return _addressPrefix; }
        }
    }

来看一下编译器的工作:

可以看到类Code_06_04是标准、明白、彻底地对类Code_06_03的继承。两个重写的方法Add和GetAddress都是普通的对象方法,只是依然被当作虚方法来看待,来看一下Add方法的IL:

.method public hidebysig virtual instance int32 
        Add(int32 a,
            int32 b) cil managed
{
//省略
}

方法GetAddress的IL:

.method family hidebysig virtual instance string 
        GetAddress(string addressID) cil managed
{
//省略
}

因为这两个方法保持着虚方法的特性,所以对于Code_06_04类的子类,同样还可以重写这两个方法。

属性成员Address这里还是一普通的对象属性。

接口的实现

对接口的实现跟对抽象类的实现相似,如下是对接口ICode_06_01的实现类:

View Code
public class Code_06_02 : ICode_06_01
    {
        string _name;
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }

        public int Add(int a, int b)
        {
            OnAdded();
            return a + b;
        }

        public event EventHandler AddEvent;
        void OnAdded()
        {
            EventHandler handler = AddEvent;
            if (handler != null)
            {
                handler(this, null);
            }
        }
    }

来看一下编译器的工作:

它与普通类的区别不大,只是很明确的是实现了接口ICode_06_01,来看一下它的IL:

.class public auto ansi beforefieldinit ConsoleApp.Example06.Code_06_02
       extends [mscorlib]System.Object
       implements ConsoleApp.Example06.ICode_06_01
{
} // end of class ConsoleApp.Example06.Code_06_02

可以看到,类Code_06_02不仅继承于System.Object类,同时还实现了接口ICode_06_01。再来看一下对于接口中的方法,编译器是如何处理的。Add.IL:

.method public hidebysig newslot virtual final 
        instance int32  Add(int32 a,
                            int32 b) cil managed
{
//省略
}

编译器认为Add方法具有虚方法的特性。而对于属性和事件,依然是普通的实现,如get/set、add/remove。

另外,接口还支持显示实现接口,我们上面讨论的Code_06_02类对接口的实现默认是隐式实现。

在接口的实现类内部,可以存在一个与接口某一方法名(包括签名)完全相同的方法,但要求那个对接口实现的方法必须是显示实现,如下代码:

        public int Add(int a, int b)
        {
            return a + b;
        }

        int ICode_06_01.Add(int a, int b)
        {
            OnAdded();
            return a + b;
        }

可以看出显示实现就是在方法前加上接口名的前缀和点号(ICode_06_01.),同时也可以看到,显示实现接口的方法是不能有可访问修饰符的,编译器会对其进行private处理。那如何才能调用显示实现的接口方法呢?可以将实现类的对象转为一个接口变量,再调用该变量的相应方法。如下代码:

            Code_06_02 code0602 = new Code_06_02();
            ICode_06_01 icode0602 = code0602;
            icode0602.Add(1, 2);

而对于抽象类的实现,是不能进行显示实现的!

 

第四节 应用中的区别

1) 抽象类保留一普通类的部分特性,定义可能已经实现的方法行为,方法内可以对数据成员(如属性)进行操作,且方法可以相互沟通。而接口仅仅是定义方法的签名,就像规则,只是约定,并没有实现。

2) 抽象类的派生类可以原封不动地得到抽象类的部分成员,接口的实现类如果想要得到接口的数据成员,则必须对其进行重写。

3) 一个类只能继承于一个抽象(类),但可以实现多个接口,并且可以在继承一个基类的基础上,同时实现多个接口。

4) 抽象类和接口都不能对其使用密封sealed,事实上这两者都是为了被其他类继承和实现,对其使用sealed是没有任何意义的。

5) 抽象类可以对接口进行实现。

6) 抽象类更多的用于“复制对象副本”,就是我们常说的“子类与父类有着is a的关系”,它更多关注于一个对象的整体特性。接口更多倾向于一系列的方法操作,这些操作在当前上下文中既有着相同作用对象,又相互隔离。

7) 某些时候,抽象类可以与接口互换

下面我们通过生活中见的红娘拉线的示例,来说明抽象类与接口给我们变成带来的方便性,下面代码既可以使用抽象类也可以使用接口。

一般(这里说明的是一般性况下)只要想通过红娘接线找到自己另一半的,在红娘(Matchmaker)安排甲去见已以前,都会安排他/她(wooer)应该怎么跟对方沟通,加深对方对自己的好感印象,这些话是红娘提供的,但说的动作,还是求婚者通过自己的“说话”本能来表达,只是红娘在教求婚者说哪些套话而已。

一般我们会如下定义:

View Code
/// <summary>
    /// 红娘
    /// </summary>
    public class Matchmaker
    {
        string message;

        /// <summary>
        /// 指导求婚人如何表达
        /// </summary>
        public void Teach()
        {
            message = "从见你的第一眼起,我就认为你会是那个每天早上跟我一起下床的人。但这绝对不是一见钟情,那是什么?我会在我们老去的那一天告诉你。";
            Wooer wooer = new Wooer();
            wooer.Say(message);
        }
    }
    /// <summary>
    /// 求婚人
    /// </summary>
    public class Wooer
    {
        /// <summary>
        /// 向对方表达
        /// </summary>
        /// <param name="message"></param>
        public void Say(string message)
        {
            Console.WriteLine(message);
        }
}

客户程序:

    public class ClientApp
    {

        public void Work()
        {
            Matchmaker matchmaker = new Matchmaker();
            matchmaker.Teach();
        }
    }

我们通常都会这么写,简单明了,实现功能没问题。

红娘在想:老娘一个人怎么能应付得了你们这一群剩男剩女!个个都有特殊情况,我哪里真有分身术!干脆我搭建一个平台,你们按我的要求填表,按我的步骤走就行了

然而这里的红娘Matchmaker是强依赖Wooer。因为毕竟无论求婚人是男是女,不仅要口头语言表达,还有自己独特的肢体语言表达,比如男人会哼哼嗨嗨地表达,女人会哼哼唧唧地表达,Wooer要变了,那这个时候Matchmaker就得变。这个要求就是把所有求婚人抽象出来,下面我们引入接口,将Wooer抽象出来为一个接口IWooer,让Matchmaker依赖于IWooer,而不直接依赖于具体求婚人。接下来如下改造代码:

View Code
/// <summary>
    /// 红娘
    /// </summary>
    public class Matchmaker
    {
        string message;
        /// <summary>
        /// 指导求婚人如何表达
        /// </summary>
        public void Teach(IWooer wooer)
        {
            message = "从见你的第一眼起,我就认为你会是那个每天早上跟我一起下床的人。但这绝对不是一见钟情,那是什么?我会在我们老去的那一天告诉你。";
            wooer.Say(message);
        }
    }
    /// <summary>
    /// 求婚人
    /// </summary>
    public interface IWooer
    {
        /// <summary>
        /// 求婚人自己准备的语言,有自己的真家伙拿出来实战!
        /// </summary>
        string Message { get; }
        /// <summary>
        /// 肢体表达
        /// </summary>
        void Action();
        /// <summary>
        /// 语言表达
        /// </summary>
        /// <param name="message"></param>
        void Say(string message);
    }

    public class ManWooer : IWooer
    {
        public string Message
        {
            get { return "我拥有制造印超机的技术"; }
        }
        public void Action()
        {
            //抱着对方、动手动脚、眼神像弯尺
            //这些动作无法用代码表达,你懂的,如饿狼一样。。。
        }
        public void Say(string message)
        {
            //先动起来预热
            this.Action();
            //接着再表达
            Console.WriteLine(message + this.Message);
        }
    }
    public class WomanWooer : IWooer
    {
        public string Message
        {
            get { return "我拥有1+2个点"; }
        }
        public void Action()
        {
            //侧身向着对方、朦胧的眼神含情默默、相见恨晚,总是想看一眼对方,但害羞的脸蛋红红
            //这些动作无法用代码表达,你懂的,如羔羊一样。。。
        }
        public void Say(string message)
        {
            //先动起来预热
            this.Action();
            //接着再表达
            Console.WriteLine(message + this.Message);
        }
    }
再来看看客户程序:
    public class ClientApp
    {

        public void Work()
        {
            Matchmaker matchmaker = new Matchmaker();

            //matchmaker.Teach();

            IWooer wooer;
            //求婚者是男人
            wooer = new ManWooer();
            matchmaker.Teach(wooer);

            //求婚者是女人
            wooer = new ManWooer();
            matchmaker.Teach(wooer);
            matchmaker.Teach(wooer);
        }
    }

很明显,现在Matchmaker已经不再直接依赖Wooer了,只依赖第三方IWooer,不论来的是男人还是女人,只要你按照红娘要求的步骤就行了,如此开放的平台,说不定明年就能创业板闯关成功!

 

小 结
posted @ 2012-08-01 18:07  solan3000  阅读(3946)  评论(17编辑  收藏  举报