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

C#中的封装继承与多态性

2024-10-296.9k 阅读

封装

封装的概念

在C#中,封装是一种将数据和操作数据的方法绑定在一起,并对外部隐藏数据实现细节的机制。简单来说,它把相关的变量和方法组合成一个单元,也就是类,并且通过访问修饰符来控制对这些成员的访问。这样做的目的是保护数据的完整性和安全性,防止外部代码对数据进行不恰当的访问和修改。

例如,考虑一个简单的“人”类,其中可能包含姓名、年龄等属性。如果不进行封装,外部代码可以随意修改这些属性,可能导致数据的不一致或不合理。通过封装,我们可以限制对这些属性的访问,只允许通过特定的方法来修改,从而保证数据的正确性。

访问修饰符

  1. public:公共访问修饰符,表示该成员可以被任何其他代码访问。这是最宽松的访问级别。
public class PublicClass
{
    public int PublicField;
    public void PublicMethod()
    {
        Console.WriteLine("This is a public method.");
    }
}

在上述代码中,PublicFieldPublicMethod可以在程序的任何地方被访问,只要有PublicClass的实例。

  1. private:私有访问修饰符,意味着该成员只能在声明它的类内部被访问。这是最严格的访问级别。
public class PrivateClass
{
    private int privateField;
    private void privateMethod()
    {
        Console.WriteLine("This is a private method.");
    }
    public void AccessPrivateMembers()
    {
        privateField = 10;
        privateMethod();
    }
}

在这个例子中,privateFieldprivateMethod只能在PrivateClass内部被访问。外部代码无法直接访问它们,只能通过AccessPrivateMembers这样的公共方法间接访问。

  1. protected:受保护访问修饰符,表明该成员可以被声明它的类及其派生类访问。
public class BaseClass
{
    protected int protectedField;
    protected void protectedMethod()
    {
        Console.WriteLine("This is a protected method.");
    }
}
public class DerivedClass : BaseClass
{
    public void AccessProtectedMembers()
    {
        protectedField = 20;
        protectedMethod();
    }
}

这里,DerivedClass可以访问BaseClass中的protectedFieldprotectedMethod,但其他无关的类不能访问。

  1. internal:内部访问修饰符,意味着该成员可以被同一程序集内的任何代码访问,但不能被其他程序集访问。如果我们有一个类库项目,内部成员在该类库内部是可见的,但对于引用该类库的外部项目是不可见的。
internal class InternalClass
{
    internal int internalField;
    internal void internalMethod()
    {
        Console.WriteLine("This is an internal method.");
    }
}
  1. protected internal:这是protectedinternal的组合,意味着该成员可以被同一程序集内的任何代码访问,也可以被其他程序集中该类的派生类访问。

属性(Properties)实现封装

属性是C#中实现封装的重要方式。属性提供了一种灵活的机制来读取、写入或计算私有字段的值。属性看起来像字段,但实际上是方法,这样可以在访问属性时执行额外的逻辑。

public class Person
{
    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            if (!string.IsNullOrEmpty(value))
            {
                name = value;
            }
            else
            {
                throw new ArgumentException("Name cannot be empty.");
            }
        }
    }
    private int age;
    public int Age
    {
        get { return age; }
        set
        {
            if (value > 0 && value < 120)
            {
                age = value;
            }
            else
            {
                throw new ArgumentException("Invalid age.");
            }
        }
    }
}

在上述Person类中,NameAge属性封装了对私有字段nameage的访问。在设置属性值时,进行了数据验证,确保数据的合理性。

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "John";
        person.Age = 30;
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }
}

通过属性,外部代码可以方便地访问和修改对象的状态,同时类内部可以控制数据的完整性。

继承

继承的概念

继承是C#中一个重要的特性,它允许一个类从另一个类获取成员(属性、方法等)。被继承的类称为基类(或父类),继承的类称为派生类(或子类)。通过继承,派生类可以重用基类的代码,并且可以添加新的成员或修改继承的成员。

继承的主要优点是代码复用和层次结构的组织。例如,我们有一个Animal类,包含一些通用的属性和方法,如NameMakeSound。然后我们可以创建DogCat等派生类,这些派生类继承自Animal类,并且可以根据自身特点实现MakeSound方法。

基类和派生类的定义

在C#中,使用:符号来表示一个类继承自另一个类。

public class Animal
{
    public string Name { get; set; }
    public void MakeSound()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}
public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("The dog barks.");
    }
    public override void MakeSound()
    {
        Console.WriteLine("The dog barks woof woof.");
    }
}

在上述代码中,Dog类继承自Animal类。Dog类不仅拥有Animal类的Name属性和MakeSound方法,还添加了自己的Bark方法。并且Dog类重写了MakeSound方法,提供了更具体的实现。

访问基类成员

派生类可以访问基类的非私有成员。在派生类中,可以使用base关键字来访问基类的成员。

public class Vehicle
{
    public string Brand { get; set; }
    public virtual void Drive()
    {
        Console.WriteLine("The vehicle is driving.");
    }
}
public class Car : Vehicle
{
    public int NumberOfSeats { get; set; }
    public override void Drive()
    {
        base.Drive();
        Console.WriteLine($"The car {Brand} with {NumberOfSeats} seats is driving.");
    }
}

Car类的Drive方法中,通过base.Drive()调用了基类VehicleDrive方法,然后添加了自己的逻辑。这样既复用了基类的代码,又进行了扩展。

构造函数和继承

当创建派生类的实例时,会先调用基类的构造函数,然后再调用派生类的构造函数。我们可以在派生类的构造函数中使用base关键字来调用基类的特定构造函数。

public class Shape
{
    public string Color { get; set; }
    public Shape(string color)
    {
        Color = color;
    }
}
public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(string color, double radius) : base(color)
    {
        Radius = radius;
    }
}

Circle类的构造函数中,通过: base(color)调用了Shape类的构造函数,传递了颜色参数。这样可以确保基类的初始化逻辑被正确执行。

继承的限制

  1. C#中只支持单继承,即一个类只能有一个直接基类。这避免了多重继承带来的复杂性和冲突,如菱形继承问题。
  2. 不能继承sealed类。sealed类表示该类不能被其他类继承,例如System.String类就是sealed的,不能有派生类。

多态性

多态性的概念

多态性是C#面向对象编程的核心特性之一,它允许通过基类的引用调用派生类的方法。简单来说,同一操作作用于不同的对象,可以有不同的解释和实现,从而产生不同的执行结果。多态性使得代码更加灵活和可扩展,提高了软件的可维护性。

多态性主要通过方法重写(override)和接口实现来体现。它分为编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态)

编译时多态主要通过方法重载(overload)来实现。方法重载是指在同一个类中,有多个方法具有相同的名称,但参数列表不同(参数的个数、类型或顺序不同)。编译器在编译时根据调用方法时传递的参数来决定调用哪个方法。

public class MathOperations
{
    public int Add(int a, int b)
    {
        return a + b;
    }
    public double Add(double a, double b)
    {
        return a + b;
    }
    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
}

在上述MathOperations类中,有三个Add方法,它们的参数列表不同。

class Program
{
    static void Main()
    {
        MathOperations mathOps = new MathOperations();
        int result1 = mathOps.Add(2, 3);
        double result2 = mathOps.Add(2.5, 3.5);
        int result3 = mathOps.Add(2, 3, 4);
        Console.WriteLine($"Result1: {result1}, Result2: {result2}, Result3: {result3}");
    }
}

编译器根据传递的参数类型和个数,在编译时就能确定调用哪个Add方法。

运行时多态(动态多态)

运行时多态主要通过方法重写(override)和虚方法(virtual)来实现。当一个方法在基类中被声明为virtual,派生类可以使用override关键字来提供该方法的不同实现。在运行时,根据对象的实际类型来决定调用哪个版本的方法。

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a shape.");
    }
}
public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}
public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

在上述代码中,Shape类的Draw方法被声明为virtualCircleRectangle类重写了这个方法。

class Program
{
    static void Main()
    {
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();
        shape1.Draw();
        shape2.Draw();
    }
}

这里,shape1shape2虽然都是Shape类型的引用,但在运行时,根据它们实际指向的对象类型(CircleRectangle),分别调用了CircleRectangle类的Draw方法。

抽象类和抽象方法

抽象类是一种不能被实例化的类,它主要作为其他类的基类,为派生类提供一个通用的框架。抽象类可以包含抽象方法,抽象方法只有声明,没有实现,必须在派生类中被重写。

public abstract class AbstractShape
{
    public abstract void Draw();
}
public class Triangle : AbstractShape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle.");
    }
}

在上述代码中,AbstractShape是抽象类,它的Draw方法是抽象方法。Triangle类继承自AbstractShape类,并实现了Draw方法。

接口与多态

接口是一种特殊的抽象类型,它只包含方法、属性等的声明,不包含实现。一个类可以实现多个接口,从而实现多态性。接口常用于实现不相关类之间的功能共享。

public interface IDrawable
{
    void Draw();
}
public class Square : IDrawable
{
    public void Draw()
    {
        Console.WriteLine("Drawing a square.");
    }
}
public class Star : IDrawable
{
    public void Draw()
    {
        Console.WriteLine("Drawing a star.");
    }
}

在上述代码中,SquareStar类都实现了IDrawable接口,它们都必须实现Draw方法。这样,通过IDrawable接口的引用,可以调用不同类的Draw方法,实现多态。

class Program
{
    static void Main()
    {
        IDrawable drawable1 = new Square();
        IDrawable drawable2 = new Star();
        drawable1.Draw();
        drawable2.Draw();
    }
}

通过封装、继承和多态性,C#提供了强大的面向对象编程能力,使得代码更加模块化、可复用、可维护和可扩展。开发者可以根据具体的需求,灵活运用这些特性来构建高效、健壮的软件系统。无论是小型的控制台应用程序,还是大型的企业级应用,这些特性都起着至关重要的作用。在实际开发中,深入理解和熟练运用封装、继承和多态性,能够显著提高开发效率和代码质量。例如,在开发图形绘制库时,通过继承和多态性可以方便地处理不同形状的绘制,通过封装可以保护图形的内部状态不被随意篡改。在企业级应用开发中,通过合理的继承结构可以复用通用的业务逻辑,通过接口实现多态可以方便地替换不同的实现策略,如数据访问层的不同数据库实现。总之,掌握C#中的封装、继承和多态性是成为优秀C#开发者的必经之路。