C#编程精要:深入理解继承、多态、抽象和接口

发布时间 2023-11-20 22:20:20作者: 52Hertz程序人生

文章目录

继承(Inheritance)

继承是OOP中的重要概念,它允许一个类(子类)从另一个类(父类)继承属性和方法。在C#中,通过 ":" 符号实现继承。这使得代码重用变得更加简单,同时也能建立类之间的层次结构,使得程序更易于理解和扩展。

当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类。

继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。

基类和派生类

一个类可以派生自多个类或接口,这意味着它可以从多个基类或接口继承数据和函数。

<访问修饰符符> class <基类>
{
 ...
}
class <派生类> : <基类>
{
 ...
}

代码实例:

using System;
namespace RectangleApplication
{
   class Rectangle
   {
      // 成员变量
      protected double length;
      protected double width;
      public Rectangle(double l, double w)
      {
         length = l;
         width = w;
      }
      public double GetArea()
      {
         return length * width;
      }
      public void Display()
      {
         Console.WriteLine("长度: {0}", length);
         Console.WriteLine("宽度: {0}", width);
         Console.WriteLine("面积: {0}", GetArea());
      }
   }

//end class Rectangle class Tabletop : Rectangle { private double cost; public Tabletop(double l, double w) : base(l, w) { } public double GetCost() { double cost; cost = GetArea() * 70; return cost; } public void Display() { base.Display(); Console.WriteLine("成本: {0}", GetCost()); } }
class ExecuteRectangle { static void Main(string[] args) { Tabletop t = new Tabletop(4.5, 7.5); t.Display(); Console.ReadLine(); } } }

执行结果:

长度: 4.5
宽度: 7.5
面积: 33.75
成本: 2362.5

多重继承

C# 不支持多重继承。但是,您可以使用接口来实现多重继承。多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。
实例:

using System;
namespace InheritanceApplication
{
   class Shape
   {
      public void setWidth(int w)
      {
         width = w;
      }
      public void setHeight(int h)
      {
         height = h;
      }
      protected int width;
      protected int height;
   }

   // 基类 PaintCost
   public interface PaintCost
   {
      int getCost(int area);

   }
   // 派生类
   class Rectangle : Shape, PaintCost
   {
      public int getArea()
      {
         return (width * height);
      }
      public int getCost(int area)
      {
         return area * 70;
      }
   }
   class RectangleTester
   {
      static void Main(string[] args)
      {
         Rectangle Rect = new Rectangle();
         int area;
         Rect.setWidth(5);
         Rect.setHeight(7);
         area = Rect.getArea();
         // 打印对象的面积
         Console.WriteLine("总面积: {0}",  Rect.getArea());
         Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area));
         Console.ReadKey();
      }
   }
}

执行结果:

总面积: 35
油漆总成本: $2450

子类调用父类构造器

public class Student : Person
{
    public Student(string name,string gender): base(name, gender)
    {
        // 该方法体执行前会先调用父类构造函数。
    }
}

子类调用父类方法

public class Student : Person
{
    public void PrintName()
    {
        // 调用父类方法
        base.PrintName(this.Name);
    }
}

注意:静态方法与实例无关,所以不能使用base。

多态(Polymorphism)

多态性允许不同的类共享相同的接口,但表现出不同的行为。在C#中,多态性可通过继承和接口来实现。这使得程序能够根据上下文以及实际对象类型来选择调用哪个方法。

父类的同一种行为,在不同的子类上有不同的实现,这种实现叫做多态。
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。
多态性可以是静态的或动态的。在静态多态性中,函数的响应是在编译时发生的。在动态多态性中,函数的响应是在运行时发生的。
在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。

静态多态性

在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C# 提供了两种技术来实现静态多态性。分别为:

  • 函数重载
  • 运算符重载

函数重载

您可以在同一个范围内对相同的函数名有多个定义。函数的定义必须彼此不同,可以是参数列表中的参数类型不同,也可以是参数个数不同。不能重载只有返回类型不同的函数声明。
下面的实例演示了几个相同的函数 Add(),用于对不同个数参数进行相加处理:

using System;
namespace PolymorphismApplication
{
    public class TestData  
    {  
        public int Add(int a, int b, int c)  
        {  
            return a + b + c;  
        }  
        public int Add(int a, int b)  
        {  
            return a + b;  
        }  
    }  
    class Program  
    {  
        static void Main(string[] args)  
        {  
            TestData dataClass = new TestData();
            int add1 = dataClass.Add(1, 2);  
            int add2 = dataClass.Add(1, 2, 3);

            Console.WriteLine("add1 :" + add1);
            Console.WriteLine("add2 :" + add2);  
        }  
    }  
}

下面的实例演示了几个相同的函数 print(),用于打印不同的数据类型:

using System;
namespace PolymorphismApplication
{
   class Printdata
   {
      void print(int i)
      {
         Console.WriteLine("输出整型: {0}", i );
      }

      void print(double f)
      {
         Console.WriteLine("输出浮点型: {0}" , f);
      }

      void print(string s)
      {
         Console.WriteLine("输出字符串: {0}", s);
      }
      static void Main(string[] args)
      {
         Printdata p = new Printdata();
         // 调用 print 来打印整数
         p.print(1);
         // 调用 print 来打印浮点数
         p.print(1.23);
         // 调用 print 来打印字符串
         p.print("Hello Runoob");
         Console.ReadKey();
      }
   }
}

运算符重载

C#为开发者提供了运算符重载的能力,开发者可以手动修改两个对象运算后的结果,而不是简单的对对象的引用地址做运算。重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的。与其他函数一样,重载运算符有返回类型和参数列表。
比如,有一个类叫做Line,两个Line的实例化相加后的结果是两个地址相加,毫无作用。通过运算符重载我们就可以使Line对象相加的结果变为两条线的总长度。大致写法如下:

// Box类的对象相加后的结果会返回一个新的Box对象,其长宽高为两个传入参数Box的长宽高相加。
public static Box operator+ (Box b, Box c)
{
   Box box = new Box();
   box.length = b.length + c.length;
   box.breadth = b.breadth + c.breadth;
   box.height = b.height + c.height;
   return box;
}

实例

using System;

namespace OperatorOvlApplication
{
   class Box
   {
      private double length;      // 长度
      private double breadth;     // 宽度
      private double height;      // 高度

      public double getVolume()
      {
         return length * breadth * height;
      }
      public void setLength( double len )
      {
         length = len;
      }

      public void setBreadth( double bre )
      {
         breadth = bre;
      }

      public void setHeight( double hei )
      {
         height = hei;
      }
      // 重载 + 运算符来把两个 Box 对象相加
      public static Box operator+ (Box b, Box c)
      {
         Box box = new Box();
         box.length = b.length + c.length;
         box.breadth = b.breadth + c.breadth;
         box.height = b.height + c.height;
         return box;
      }

   }

   class Tester
   {
      static void Main(string[] args)
      {
         Box Box1 = new Box();         // 声明 Box1,类型为 Box
         Box Box2 = new Box();         // 声明 Box2,类型为 Box
         Box Box3 = new Box();         // 声明 Box3,类型为 Box
         double volume = 0.0;          // 体积

         // Box1 详述
         Box1.setLength(6.0);
         Box1.setBreadth(7.0);
         Box1.setHeight(5.0);

         // Box2 详述
         Box2.setLength(12.0);
         Box2.setBreadth(13.0);
         Box2.setHeight(10.0);

         // Box1 的体积
         volume = Box1.getVolume();
         Console.WriteLine("Box1 的体积: {0}", volume);

         // Box2 的体积
         volume = Box2.getVolume();
         Console.WriteLine("Box2 的体积: {0}", volume);

         // 把两个对象相加
         Box3 = Box1 + Box2;

         // Box3 的体积
         volume = Box3.getVolume();
         Console.WriteLine("Box3 的体积: {0}", volume);
         Console.ReadKey();
      }
   }
}

执行结果:

Box1 的体积: 210
Box2 的体积: 1560
Box3 的体积: 5400

动态多态性

C# 允许您使用关键字 abstract 创建抽象类,用于提供接口的部分类的实现。当一个派生类继承自该抽象类时,实现即完成。抽象类包含抽象方法,抽象方法可被派生类实现。派生类具有更专业的功能。

请注意,下面是有关抽象类的一些规则:

  • 您不能创建一个抽象类的实例。
  • 您不能在一个抽象类外部声明一个抽象方法。
  • 通过在类定义前面放置关键字 sealed,可以将类声明为密封类。当一个类被声明为 sealed 时,它不能被继承。抽象类不能被声明为 sealed。

抽象类(Abstraction)

抽象类表示一个概念的抽象,只表示做什么,拥有什么数据,但往往不表达具体做法。
语法:

  • 用abstract 修饰类即为抽象类。
  • 抽象类中可能包含抽象成员(方法、属性)。
  • 抽象类不能实例化对象。

适用性:

  • 当有行为,但是不需要实现的时候。
  • 当有一些行为,在做法上有很多种可能时,但又不希望调用方了解具体做法。
  • 不希望类创建对象时。

抽象属性

抽象属性相当于抽象了属性的get、set方法,对于属性的使用没有影响。例如:

// 定义抽象类
public abstract class Shape
{
    public abstract double Area
    {
        get;
    }
 
    public override string ToString()
    {
        return Id + " Area = " + string.Format("{0:F2}", Area);
    }
}

// 定义子类
public class Square : Shape
{
    private int side;
    public Square(int side, string id)
        : base(id)
    {
        this.side = side;
    }
 
     // 重写Area属性,添加get方法的实现
    public override double Area
    {
        get
        {
            return side * side;
        }
    }
}

抽象方法

语义:

  • 描述做什么,不描述怎么做。
  • 一个行为的抽象。
  • 抽象方法要求子类必须实现,通常是本类无法实现时才需要用到。

语法:

  • 用abstract 修饰并且没有实现的方法,只有方法声明,没有实现。
  • 抽象方法只能出现在抽象类中。
  • 抽象方法在本类中不实现,在子类中用override重写实现。
// 怪物类(抽象)
abstract class Monster
{
    // 定义攻击方法
    abstract public void attack();
}

// 剑型怪物
class SwordMonster:  Monster
{
    // 实现剑型怪物的攻击方法
    public override void attack()
    {
        Console.WriteLine("俺用长剑攻击");
    }
}

虚方法

虚方法相当于提供了默认实现的抽象方法,不强制子类实现。用法与抽象方法基本相同。

// 怪物类(不需要抽象)
class Monster
{
    // 定义虚方法
    virtual public void attack()
    {
        Console.WriteLine("俺赤手空拳攻击玩家");
    }
}

// 剑型怪物
class SwordMonster:  Monster
{
    // 重写剑型怪物的攻击方法
    public override void attack()
    {
        Console.WriteLine("俺用长剑攻击玩家");
    }
}

方法重写

用关键字 virtual 或 abstract 修饰的方法或者接口中的方法,可以在子类中用 override 声明同名的方法,这叫“重写”。相应的没有用virtual修饰的方法,我们叫它实方法。
重写会改变父类方法的功能,当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能。

方法覆盖

在子类中用 new 关键字修饰 定义的与父类中同名的方法,叫覆盖。 覆盖只能覆盖有方法体的方法(虚方法和实方法),不能覆盖没有方法体的方法(抽象方法、接口方法)。
覆盖不会改变父类方法的功能。当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能;而覆盖不会,仍然调用父类功能。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp4
{
    public class GetTheName
    {
        public virtual string ReWrite_Get()   // 实现重写的虚方法
        {
            return "父类";
        } 

        public string Cover_Get()        // 实现覆盖实方法
        {
            return "父类";
        } 
    }

    public class Rewrite : GetTheName   // 继承父类的重写子类
    {
        public override string ReWrite_Get()
        {
            return "父类被改变"; 
        }
    }

    public class Cover : GetTheName  // 继承父类的覆盖子类
    {
        public new string Cover_Get() 
        { 
            return "父类被改变";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            GetTheName getTheName = new GetTheName();  //  对父类实例化
            Console.WriteLine(getTheName.ReWrite_Get()); // 调用父类虚方法
            Console.WriteLine(getTheName.Cover_Get());  // 调用父类实方法

            GetTheName rewrite = new Rewrite(); // 用子类创建父类对象
            Console.WriteLine(rewrite.ReWrite_Get());

            GetTheName ovverides = new Cover(); // 用子类创建父类对象
            Console.WriteLine(ovverides.Cover_Get());
            Console.ReadKey();
        }
    }
}

覆写和覆盖的区别

  • 不管是重写还是覆盖都不会影响父类自身的功能。
  • 当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能;而覆盖不会,仍然调用父类功能。
  • 虚方法、实方法都可以被覆盖(new),抽象方法,接口 不可以。
  • 抽象方法,接口,标记为virtual的方法可以被重写(override),实方法不可以。
  • 重写使用的频率比较高,实现多态;覆盖用的频率比较低,用于对以前无法修改的类进行继承的时候。

密封类(sealed)

当一个类被生命为sealed类,就代表了这个类不允许被继承,它是最终版本,无法派生出新的子类。

class A {}
sealed class B : A {}

当你将类标记为 sealed 后,任何尝试从这个类派生出新的类都会导致编译错误。这种设计对于那些不希望其他人修改或扩展特定类的情况非常有用,尤其是当你认为这个类已经完成并且不应该再被继续扩展时。

值得注意的是,你可以使用 sealed 关键字修饰类、方法或属性。如果应用在类上,该类将无法被继承;如果应用在方法或属性上,它们将无法被重写。

总的来说,密封类是一种限制继承和重写的有效方式,可以在确保类的稳定性和一致性时使用。

接口(Interface)

接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 “是什么” 部分,派生类定义了语法合同 “怎么做” 部分。

接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。

接口使得实现接口的类或结构在形式上保持一致。

抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。

接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。

抽象类不能直接实例化,但允许派生出具体的,具有实际功能的类。

接口特点

  • 一组:接口中可以包含多个方法。
  • 对外:接口成员是要求子类实现,自己不用实现。
  • 行为:接口中只能包含抽象方法成员(属性、方法)。
  • 规范:要求子类必须自行实现(包括属性)。

is a的时候用类。通常类代表了与子类是统一类别。
has a的时候用接口。通常接口代表了一系列动作。

类与类是单继承,接口是多实现,也就是说你只能有一个类别,但是可以有多种动作。

 

定义接口

接口使用 interface 关键字声明,它与类的声明类似。接口声明默认是 public 的。下面是一个接口声明的实例:

interface IMyInterface
{
    void MethodToImplement();
}

以上代码定义了接口 IMyInterface。通常接口命令以 I 字母开头,这个接口只有一个方法 MethodToImplement(),没有参数和返回值,当然我们可以按照需求设置参数和返回值。

值得注意的是,该方法并没有具体的实现。

实现接口

using System;

interface IMyInterface
{
        // 接口成员
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
    }

    public void MethodToImplement()
    {
        Console.WriteLine("MethodToImplement() called.");
    }
}

InterfaceImplementer 类实现了 IMyInterface 接口,接口的实现与类的继承语法格式类似。继承接口后,我们需要实现接口的方法 MethodToImplement() , 方法名必须与接口定义的方法名一致。

接口继承

以下实例定义了两个接口 IMyInterface 和 IParentInterface。
如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。
以下实例 IMyInterface 继承了 IParentInterface 接口,因此接口实现类必须实现 MethodToImplement() 和 ParentInterfaceMethod() 方法:

using System;

interface IParentInterface
{
    void ParentInterfaceMethod();
}

interface IMyInterface : IParentInterface
{
    void MethodToImplement();
}

class InterfaceImplementer : IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        iImp.MethodToImplement();
        iImp.ParentInterfaceMethod();
    }

    public void MethodToImplement()
    {
        Console.WriteLine("MethodToImplement() called.");
    }

    public void ParentInterfaceMethod()
    {
        Console.WriteLine("ParentInterfaceMethod() called.");
    }
}

实例输出结果为:

MethodToImplement() called.
ParentInterfaceMethod() called.

显式实现接口

interface IMyInterface1
{
    void DoSomething();
}

interface IMyInterface2
{
    void DoSomething();
}

class InterfaceImplementer : IMyInterface1, IMyInterface2
{

    public void IMyInterface1.DoSomething()
    {
        Console.WriteLine("IMyInterface1.DoSomething() called.");
    }

    public void IMyInterface2.DoSomething()
    {
        Console.WriteLine("IMyInterface2.DoSomething() called.");
    }
}

class Prog: IMyInterface
{
    static void Main()
    {
        InterfaceImplementer iImp = new InterfaceImplementer();
        // iImp.DoSomething(); 此时是调不到这个方法的。
        
        IMyInterface1 mImpl1 = new InterfaceImplementer();
        mImpl1.DoSomething();           // 调用了IMyInterface1的方法。
        
        IMyInterface2 mImpl2 = new InterfaceImplementer();
        mImpl2.DoSomething();           // 调用了IMyInterface2的方法。
    }
}

执行结果:

IMyInterface1.DoSomething() called.
IMyInterface2.DoSomething() called.

另外,显式实现接口还有一种用法,就是隐藏方法。当一个类在实现接口的过程中不想实现其中的某个方法时,可以使用显式实现的方式实现此方法,并提供一个空实现,这样类的实例在调用方法时就看不到这个不想被使用的方法了。

所以显式实现接口有两个作用:

  • 解决读个接口实现时的二义性。
  • 解决接口中的成员对实现类不适用的问题。

通过理解和灵活运用继承、多态、抽象、密封类和接口这些概念,能够构建出更加健壮和可扩展的应用程序,提高代码的复用性并简化程序设计过程。