MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#面向对象编程:类与继承机制深度解读

2022-03-057.0k 阅读

C#面向对象编程之基础:类的构建与剖析

类的定义与基础结构

在C# 中,类是一种封装数据和相关行为的构造体,它是面向对象编程的核心。类的定义使用class关键字,例如:

class MyClass
{
    // 字段(成员变量)
    private int myField;
    // 属性
    public int MyProperty
    {
        get { return myField; }
        set { myField = value; }
    }
    // 方法
    public void MyMethod()
    {
        Console.WriteLine("This is MyMethod in MyClass.");
    }
}

在上述代码中,MyClass类包含了一个私有字段myField,一个公开属性MyProperty用于访问和修改myField,以及一个公开方法MyMethod。字段用于存储数据,属性提供了一种灵活的方式来访问和修改字段,而方法则定义了类可以执行的操作。

访问修饰符

  1. private:私有访问修饰符意味着只有类内部的代码可以访问该成员。如上述代码中的myField,外部代码无法直接访问,从而保证了数据的安全性和封装性。
  2. public:公开访问修饰符允许任何代码访问该成员。MyPropertyMyMethod使用public修饰,使得其他类可以通过创建MyClass的实例来访问它们。
  3. protected:受保护访问修饰符表示只有类本身及其派生类可以访问该成员。这在继承机制中非常重要,我们后续会详细讨论。
  4. internal:内部访问修饰符表示只有在同一个程序集内的代码可以访问该成员。程序集是一个可部署的单元,例如一个.dll文件或一个.exe文件。

构造函数与析构函数

  1. 构造函数:构造函数用于在创建类的实例时初始化对象的状态。构造函数与类名相同,没有返回类型。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 构造函数
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

在上述代码中,Person类有一个接受string类型的nameint类型的age作为参数的构造函数。当我们创建Person的实例时,可以使用这个构造函数来初始化对象的属性:

Person person = new Person("John", 30);
  1. 析构函数:析构函数用于在对象被垃圾回收器回收之前执行清理操作。析构函数不能有参数,也不能被显式调用。
class ResourceHolder
{
    // 模拟占用的资源
    private IntPtr unmanagedResource;

    ~ResourceHolder()
    {
        // 释放非托管资源
        if (unmanagedResource != IntPtr.Zero)
        {
            // 实际的资源释放代码,例如调用本地API
            Console.WriteLine("Releasing unmanaged resource.");
        }
    }
}

在上述代码中,ResourceHolder类的析构函数在对象即将被回收时释放非托管资源。然而,在现代C#编程中,通常建议使用IDisposable接口来进行资源管理,这比析构函数更加可控和高效。

深入C#类的特性与高级应用

静态成员

  1. 静态字段:静态字段属于类本身,而不是类的实例。所有实例共享同一个静态字段。
class Counter
{
    // 静态字段
    public static int Count { get; set; }

    public Counter()
    {
        Count++;
    }
}

在上述代码中,每次创建Counter的实例时,静态字段Count都会增加。可以通过类名直接访问静态字段:

Counter counter1 = new Counter();
Counter counter2 = new Counter();
Console.WriteLine(Counter.Count); // 输出 2
  1. 静态方法:静态方法同样属于类,不需要创建类的实例就可以调用。
class MathUtils
{
    public static int Add(int a, int b)
    {
        return a + b;
    }
}

调用静态方法:

int result = MathUtils.Add(3, 5); // result 为 8

嵌套类

嵌套类是在另一个类内部定义的类。嵌套类可以访问外部类的私有成员。

class OuterClass
{
    private int outerField = 10;

    // 嵌套类
    class InnerClass
    {
        public void InnerMethod()
        {
            OuterClass outer = new OuterClass();
            Console.WriteLine(outer.outerField);
        }
    }
}

可以通过以下方式使用嵌套类:

OuterClass.InnerClass inner = new OuterClass.InnerClass();
inner.InnerMethod();

密封类

密封类使用sealed关键字修饰,不能被其他类继承。这在某些情况下非常有用,例如防止某些关键类被错误地继承,或者为了性能优化(因为编译器可以对密封类进行一些特殊的优化)。

sealed class FinalClass
{
    // 类的成员
}

如果尝试从FinalClass继承,编译器会报错。

C#继承机制的全面解析

继承的基本概念与语法

继承是一种机制,通过它一个类(派生类)可以获取另一个类(基类)的成员。在C#中,使用冒号(:)来表示继承关系。

class Animal
{
    public string Name { get; set; }
    public void Eat()
    {
        Console.WriteLine(Name + " is eating.");
    }
}

class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine(Name + " is barking.");
    }
}

在上述代码中,Dog类继承自Animal类。Dog类自动获得了Animal类的Name属性和Eat方法,并且还可以定义自己特有的方法,如Bark

Dog dog = new Dog();
dog.Name = "Buddy";
dog.Eat(); // 输出 Buddy is eating.
dog.Bark(); // 输出 Buddy is barking.

重写基类成员

  1. 虚方法与重写:基类中的方法如果希望派生类可以重写,需要使用virtual关键字修饰。派生类使用override关键字来重写该方法。
class Shape
{
    public virtual double Area()
    {
        return 0;
    }
}

class Circle : Shape
{
    public double Radius { get; set; }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

在上述代码中,Shape类的Area方法是虚方法,Circle类重写了Area方法以计算圆的面积。

Circle circle = new Circle { Radius = 5 };
Console.WriteLine(circle.Area()); // 输出圆的面积
  1. 新方法隐藏:如果派生类定义了与基类中方法同名但不使用override关键字的方法,会发生新方法隐藏。这种情况下,派生类的方法会隐藏基类的方法,但通常不推荐使用,因为它可能会导致代码可读性和维护性问题。
class BaseClass
{
    public void Method()
    {
        Console.WriteLine("BaseClass Method");
    }
}

class DerivedClass : BaseClass
{
    public new void Method()
    {
        Console.WriteLine("DerivedClass Method");
    }
}
BaseClass baseObj = new DerivedClass();
baseObj.Method(); // 输出 BaseClass Method
DerivedClass derivedObj = new DerivedClass();
derivedObj.Method(); // 输出 DerivedClass Method

访问基类成员

  1. base关键字:在派生类中,可以使用base关键字来访问基类的成员。例如,在派生类的构造函数中调用基类的构造函数。
class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}

class Student : Person
{
    public int StudentId { get; set; }

    public Student(string name, int studentId) : base(name)
    {
        StudentId = studentId;
    }
}

在上述代码中,Student类的构造函数使用base(name)调用了Person类的构造函数来初始化Name属性。 2. 访问基类重写成员:当派生类重写了基类的方法后,仍然可以使用base关键字访问基类的原始方法。

class Shape
{
    public virtual double Area()
    {
        return 0;
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double Area()
    {
        // 调用基类的 Area 方法
        double baseArea = base.Area();
        return Width * Height;
    }
}

继承机制中的多态性实现

编译时多态(静态多态)

编译时多态主要通过方法重载来实现。方法重载是指在同一个类中定义多个同名但参数列表不同的方法。

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

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

在上述代码中,Calculator类有两个Add方法,一个接受两个int类型参数,另一个接受两个double类型参数。编译器会根据调用时传入的参数类型来决定调用哪个方法。

Calculator calculator = new Calculator();
int result1 = calculator.Add(3, 5); // 调用 int 版本的 Add 方法
double result2 = calculator.Add(3.5, 5.5); // 调用 double 版本的 Add 方法

运行时多态(动态多态)

运行时多态通过继承和虚方法重写来实现。当通过基类类型的变量引用派生类对象时,实际调用的方法取决于对象的实际类型,而不是变量的类型。

class Shape
{
    public virtual double Area()
    {
        return 0;
    }
}

class Circle : Shape
{
    public double Radius { get; set; }

    public override double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double Area()
    {
        return Width * Height;
    }
}
Shape[] shapes = new Shape[]
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};

foreach (Shape shape in shapes)
{
    Console.WriteLine(shape.Area());
}

在上述代码中,shapes数组包含了CircleRectangle类型的对象,但通过Shape类型的变量来引用。在循环中调用Area方法时,实际调用的是每个对象对应的重写后的Area方法,这就是运行时多态的体现。

继承关系中的类型转换

隐式转换

  1. 派生类到基类的隐式转换:由于派生类是基类的一种特殊类型,因此可以将派生类对象隐式转换为基类类型。
class Animal
{
    // 类的成员
}

class Dog : Animal
{
    // 类的成员
}
Dog dog = new Dog();
Animal animal = dog; // 隐式转换

这种转换是安全的,因为派生类对象包含了基类的所有成员。

显式转换

  1. 基类到派生类的显式转换:将基类对象转换为派生类对象需要进行显式转换。但是,只有当基类对象实际指向的是派生类对象时,这种转换才会成功,否则会抛出InvalidCastException异常。
Animal animal = new Dog();
Dog dog = (Dog)animal; // 显式转换,成功

Animal animal2 = new Animal();
Dog dog2 = (Dog)animal2; // 显式转换,会抛出 InvalidCastException
  1. 使用isas关键字:为了避免在显式转换时抛出异常,可以使用is关键字先检查对象是否可以转换,或者使用as关键字进行转换,如果转换失败会返回null而不是抛出异常。
Animal animal3 = new Dog();
if (animal3 is Dog)
{
    Dog dog3 = (Dog)animal3;
}

Animal animal4 = new Animal();
Dog dog4 = animal4 as Dog;
if (dog4 != null)
{
    // 进行操作
}

继承与接口的综合应用

接口的定义与实现

接口是一种契约,它定义了一组方法签名但不包含方法的实现。类可以实现一个或多个接口,以表明它支持这些接口定义的行为。

interface IPrintable
{
    void Print();
}

class Book : IPrintable
{
    public string Title { get; set; }

    public void Print()
    {
        Console.WriteLine("Book Title: " + Title);
    }
}

在上述代码中,IPrintable接口定义了一个Print方法,Book类实现了这个接口并提供了Print方法的具体实现。

Book book = new Book { Title = "C# Programming" };
IPrintable printable = book;
printable.Print(); // 输出 Book Title: C# Programming

继承与接口的协同工作

一个类可以在继承另一个类的同时实现接口。这使得代码的复用性和灵活性得到了极大的提升。

class Shape
{
    // 类的成员
}

interface IDrawable
{
    void Draw();
}

class Circle : Shape, IDrawable
{
    public double Radius { get; set; }

    public void Draw()
    {
        Console.WriteLine("Drawing a circle with radius " + Radius);
    }
}

在上述代码中,Circle类继承自Shape类并实现了IDrawable接口。这样Circle类既拥有了Shape类的特性,又具备了IDrawable接口定义的绘制功能。

Circle circle = new Circle { Radius = 5 };
IDrawable drawable = circle;
drawable.Draw(); // 输出 Drawing a circle with radius 5

接口继承

接口也可以继承其他接口,形成接口的继承层次结构。

interface IAnimal
{
    void Eat();
}

interface IMammal : IAnimal
{
    void Nurse();
}

class Dog : IMammal
{
    public void Eat()
    {
        Console.WriteLine("Dog is eating.");
    }

    public void Nurse()
    {
        Console.WriteLine("Dog is nursing its puppies.");
    }
}

在上述代码中,IMammal接口继承自IAnimal接口,Dog类实现了IMammal接口,因此需要实现IAnimal接口的Eat方法和IMammal接口的Nurse方法。

通过深入理解C#中的类与继承机制,开发者能够编写出更加模块化、可维护和可扩展的代码,充分发挥面向对象编程的优势。无论是小型应用程序还是大型企业级项目,这些概念都是构建稳健软件的基石。