zoukankan      html  css  js  c++  java
  • 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,不论来的是男人还是女人,只要你按照红娘要求的步骤就行了,如此开放的平台,说不定明年就能创业板闯关成功!

    小 结
  • 相关阅读:
    Train Problem(栈的应用)
    Code obfuscatio (翻译!)
    Milking Cows
    Sorting a Three-Valued Sequence(三值排序)
    Asphalting Roads(翻译!)
    FatMouse' Trade
    Fibonacci Again
    Yogurt factory
    经济节约
    Lucky Conversion(找规律)
  • 原文地址:https://www.cnblogs.com/solan/p/CSharp06.html
Copyright © 2011-2022 走看看