C#接口、抽象类与继承

本文讨论C#中的接口与实现、抽象类、接口和类的继承,以及对象类型的判断和转换等内容。

接口

接口(interface)可以看作是一系列类的定义标准,在接口中可以指定这些类必须实现的属性、方法、事件和索引器;通过接口定义的成员在类中的访问级别都是公共的(public),但在接口中定义成员时不需要使用访问级别的关键字。此外,定义接口类型需要使用interface关键字,接口名称习惯上使用大写字母I开始。下面的代码演示了接口的定义,并通过类来实现接口。

C#
using System;

namespace csfx_demo
{
    //
    interface I1
    {
        string Id { get; set; }
        void M1();
    }
    //
    class C1 : I1
    {
        public string Id { get; set; }
        public void M1()
        {
            Console.WriteLine("C1.M1, ID:{0}", Id);
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1 c = new C1() { Id = "123" };
            c.M1();
        }
    }
}

代码中首先定义了I1接口,其中包含了Id属性和M1()方法。接下来,使用C1类实现此接口,在C1类名后的冒号(:)在这里的含义为实现(implement)。C1类中,M1()方法中会显示所在类和方法名,以及Id属性的值。最后,在Main()方法中创建C1类的实例并赋值到c对象,设置Id属性为"123",并调用M1()方法显示信息,执行代码会显示“C1.M1, ID:123”。

定义新的接口时,可以从现有接口类型继承,一个接口可以同时继承多个接口类型;下面的代码演示了相关应用。

C#
using System;

namespace csfx_demo
{
    //
    interface I1
    {
        string Id { get; set; }
        void M1();
    }
    //
    interface I2
    {
        string Name { get; set; }
        void M2();
    }
    //
    interface I3 : I1, I2
    {
        void M3();
    }
    //
    class C1 : I3
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public void M1()
        {
            Console.WriteLine("C1.M1, ID:{0}", Id);
        }
        //
        public void M2()
        {
            Console.WriteLine("C1.M2, Name:{0}", Name);
        }
        //
        public void M3()
        {
            Console.WriteLine("C1.M3");
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1 c = new C1()
            {
                Id = "123",
                Name = "abc"
            };
            c.M1();
            c.M2();
            c.M3();
        }
    }
}

代码中,首先定义了I1和I2接口,然后定义的I3接口会同时继承I1和I2接口,在I3接口名称后的冒号(:)含义为继承(inherit),继承的多个接口类型使用逗号分隔。接下来的代码,使用C1类实现I3接口,需要实现I1、I2和I3接口中的所有成员。执行代码结果如下图所示

接口的继承与实现

类可以同时实现多个接口,此时需要注意不同接口类型中的同名成员,如下面的代码。

C#
using System;

namespace csfx_demo
{
    //
    interface I1
    {
        string Id { get; set; }
        void M1();
    }
    //
    interface I2
    {
        void M1();
        void M2();
    }
    //
    class C1 : I1, I2
    {
        public string Id { get; set; }
        //
        public void M1()
        {
            Console.WriteLine("I1(I2).M1, Id:{0}", Id);
        }
        //
        public void M2()
        {
            Console.WriteLine("I2.M2");
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1 c = new C1() { Id = "123" };
            c.M1();
            c.M2();
        }
    }
}

代码中的C1类同时实现了I1和I2两个接口类型,需要实现Id属性,M1()和M2()方法;这里应注意,I1和I2接口中都包含了M1()方法。

类的继承

通过继承,可以最大限度的实现代码的重复使用,像接口的继承一样,当一个类继承某个类时使用冒号(:)声明。需要注意的是,类只能继承一个类,不能同时继承多个类。在类的继承关系中,继承类称为子类,被继承的类称为基类(也称为父类、超类)。下面的代码演示了类的继承。

C#
using System;

namespace csfx_demo
{
    //
    class C1
    {
        public string Id { get; set; }
        //
        public void M1()
        {
            Console.WriteLine("C1.M1");
        }
        //
        public virtual void M2()
        {
            Console.WriteLine("C1.M2");
        }
    }
    //
    class C1a : C1
    {
        new public void M1()
        {
            base.M1();
            Console.WriteLine("C1a.M1, Id:{0}", Id);
        }
        //
        public override void M2()
        {
            base.M2();
            Console.WriteLine("C1a.M2");
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1a c = new C1a() { Id = "123" };
            c.M1();
            c.M2();
        }
    }
}

代码中,首先定义了C1类,它包含Id属性、M1()和M2()方法,其中,M2()方法定义为虚拟(virtual)方法,可以在子类中重写。

C1a类定义为C1类的子类,此时C1类称为C1a类的基类。由于C1类中的M1()方法不是虚拟(virtual)方法,也不是抽象(abstract)方法(稍后讨论),在C1a类中重新实现方法时需要使用new关键字覆盖基类中的定义,其中,使用base关键字调用了C1类中的M1()方法。由于C1类中的M2()方法定义为虚拟方法,可以在C1a类中使用override关键字重写它,方法中还使用base关键字调用了基类中的M2()方法;请注意,子类中可以调用基类中除了私有(private)成员以外的其它成员,即可以调用protected、public、internal关键字定义的成员。

Main()方法中创建了C1a类的实例并赋值给c对象,然后调用了M1()和M2()方法,执行结果如下图所示。

类的继承

抽象类

某些情况下,有些类不允许实例化,或者其中有些成员暂时无法确认具体的实现,此时可以将类定义为抽象类,没有具体实现的成员定义为抽象成员;请注意,抽象成员必须定义在抽象类中。C#中定义抽象类和抽象成员都需要使用abstract关键字,如下面的代码。

C#
using System;

namespace csfx_demo
{
    //
    abstract class C1
    {
        public string Id { get; set; }
        //
        public abstract void M1();
        //
        public virtual void M2()
        {
            Console.WriteLine("C1.M2");
        }
    }
    //
    class C1a : C1
    {
        public override void M1()
        {
            Console.WriteLine("C1a.M1, Id:{0}", Id);
        }
        //
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1a c = new C1a();
            c.M1();
            c.M2();
        }
    }
}

代码中,首先定义了抽象类C1,其中的M1()方法定义为抽象方法,请注意,抽象方法不需要使用花括号定义方法体;M2()方法定义为虚拟(virtual)方法。接下来的C1a类继承于C1类,其中使用override关键字重写了M1()方法。执行结果如下图的所示。

抽象类的继承

对于抽象类中的抽象成员和虚拟成员,都可以在子类中使用override关键字重写,如下面的代码。

C#
using System;

namespace csfx_demo
{
    //
    abstract class C1
    {
        public string Id { get; set; }
        //
        public abstract void M1();
        //
        public virtual void M2()
        {
            Console.WriteLine("C1.M2");
        }
    }
    //
    class C1a : C1
    {
        public override void M1()
        {
            Console.WriteLine("C1a.M1, Id:{0}", Id);
        }
        //
        public override void M2()
        {
            base.M2();
            Console.WriteLine("C1a.M2");
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1a c = new C1a();
            c.M1();
            c.M2();
        }
    }
}

代码执行结果如下图所示。

重写抽象成员和虚拟成员

抽象类型和接口都可以做为一系列子类的标准,但抽象类是一个具体的类型,相比之下,组件传递时使用接口会更加灵活,就像生活中的电源插座接口、USB接口等,只要设备匹配就能灵活应用。此外,接口成员没有具体的实现,抽象类则允许实现一些成员,在子类中可以直接使用这些成员。

判断对象类型

除了使用对象的GetType()方法或typeof运算符获取类型的Type对象,还可以使用is运算符判断对象是否可以转换为指定的类型,运算结果为bool类型,可以转换为目标类型时返回true,否则返回false。下面的代码演示了is运算符的使用。

C#
using System;

namespace csfx_demo
{
    //
    abstract class C1
    {
        public string Id { get; set; }
        //
        public abstract void M1();
        //
        public virtual void M2()
        {
            Console.WriteLine("C1.M2");
        }
    }
    //
    class C1a : C1
    {
        public override void M1()
        {
            Console.WriteLine("C1a.M1, Id:{0}", Id);
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1a c = new C1a();
            Console.WriteLine(c is C1a);  // True
            Console.WriteLine(c is C1);  // True
            Console.WriteLine(c is Object);  // True
            Console.WriteLine(c is String);  // False
        }
    }
}

代码中,c对象定义为C1a类的实例,而C1a类定义为C1类的子类,使用is运算符时,对象不但是其定义类型的实例,同样也可以视作其基类的实例,而C#和.NET Framework环境中,所有类的终极基类是Object,所以,第三个输出同样是True,对象的ToString()、Equals()、GetType()方法都源自Object类。最后一个输出,由于C1a类的实例无法直接转换为String类型,所以会输出False。

如果创建类时没有指定基类,默认会继承Object类,所以,本例中C1a类的继承关系为:C1a继承于C1,C1继承于Object;而Object类是唯一没有基类的类。

转换对象类型

as运算符可以转换引用类型对象或可空类型变量的类型。将子类对象转换为基类类型后,对象不能调用基类中的虚拟(virtual)和抽象(abstract)成员。下面的代码演示了as运算符的应用。

C#
using System;

namespace csfx_demo
{
    //
    abstract class C1
    {
        public string Id { get; set; }
        //
        public abstract void M1();
        //
        public virtual void M2()
        {
            Console.WriteLine("C1.M2");
        }
        //
        public void M3()
        {
            Console.WriteLine("C1.M3");
        }
    }
    //
    class C1a : C1
    {
        public override void M1()
        {
            Console.WriteLine("C1a.M1, Id:{0}", Id);
        }
        //
        public override void M2()
        {
            base.M2();
            Console.WriteLine("C1a.M2");
        }
        //
        new public void M3()
        {
            Console.WriteLine("C1a.M3");
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1a c = new C1a();
            (c as C1).M1();
            (c as C1).M2();
            (c as C1).M3();
        }
    }
}

代码执行结果如下图所示。

as运算符

代码中,C1类中的M1()方法定义为抽象方法,M2()方法定义为虚拟方法,M3()方法为普通实例方法。C1a类中重写了M1()和M2()方法。最后,在Main()方法中定义c对象为C1a类的实例,然后通过as运算符转换为C1类型,可以看到,转换类型后的对象调用的M1()和M2()方法实际仍为C1a类中的实现,而M3()方法则是C1类中的实现,即使在C1a类中使用new关键字覆盖C1类中的M3()方法,此处依然会调用C1类的M3()方法。