CodeGym /课程 /C# SELF /抽象方法和属性

抽象方法和属性

C# SELF
第 22 级 , 课程 2
可用

1. 抽象方法

在上一节课里我们打好了基础:认识了抽象类,还看到它们可以包含抽象方法和属性。我们明白了,这其实就是个“契约”,强制子类必须给出自己的实现。

现在我们来深入点,看看更复杂、更实用的场景。这节课我们会专注于语法细节,抽象在多层继承体系下是怎么工作的,还有什么时候该用abstract,什么时候用virtual

抽象方法就像一本老食谱里的神秘配方:“加点秘密佐料”——到底加啥没说,但后面的厨师都得自己想办法!

抽象方法只能在抽象类里

如果你想在普通(非抽象)类里声明抽象方法,编译器会直接报错。为啥?因为普通类可以直接实例化,那你要是调用了没写实现的方法会咋样?IDE只会给你个懵逼的眼神。


// 这样会编译报错:
public class WrongClass
{
    public abstract void Oops(); // 不能这么写!
}

只要类里有一个抽象方法,这个类就必须标记成abstract

在子类里实现抽象方法

实现的时候要用override关键字。如果你继承了抽象类但没实现所有抽象方法,这个类也必须是抽象的(不然编译器会不高兴)。

继续咱们的应用思路:

前面我们写了个Shape(图形)类,里面声明了个计算面积的抽象方法。现在来写几个具体的图形类:


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

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

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

    public Circle(double radius)
    {
        Radius = radius;
    }

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

为啥抽象方法这么重要?

抽象方法就是你对继承者说:“兄弟,你要继承这个类,得自己决定这玩意儿咋干!”这样让程序结构更清晰,防止漏写实现,最重要的是能让多态玩得飞起。

2. 抽象属性(properties)

啥是抽象属性

抽象属性其实和抽象方法一样,也是个契约,只不过是针对属性(property)的。C#里属性是封装数据的主要方式,抽象属性就是告诉子类:“你们自己决定这个属性的值怎么来(甚至怎么设置)”。

举个例子:
给图形加个Name属性——每个子类都得有,但怎么返回名字让他们自己决定。


public abstract class Shape
{
    public abstract string Name { get; }

    public abstract double CalculateArea();
}

现在每个子类都必须实现这个属性:


public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }
    public override string Name => "矩形";

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; }
    public override string Name => "圆";

    public Circle(double radius)
    {
        Radius = radius;
    }

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

语法小细节

抽象属性和接口属性很像:声明时没有实现体,但要写清楚是只有getter,还是还有setter


public abstract class Creature
{
    // 只读属性
    public abstract string Species { get; }

    // 可读可写属性
    public abstract int Age { get; set; }
}

在实现类里你可以写自己的获取(和必要时的设置)逻辑。

为啥要用抽象属性

抽象属性超适合让每个子类自己决定怎么获取或存储值。建模型、做业务逻辑、MVVM啥的都很常用,尤其是属性的逻辑比直接返回字段复杂时。

比如有的图形名字是算出来的,有的是常量,都能优雅地“藏”在抽象属性后面。

3. 图解:抽象类、方法和属性的关系

来看看下面这张图,直观感受下它们是怎么协作的:


┌─────────────┐
│ abstract    │
│   Shape     │
│-------------│
│ +Name: str  │  <-- 抽象属性
│ +Area(): dbl│  <-- 抽象方法
└─────┬───────┘
      │
 ┌────▼────┐      ┌───────┐
 │Rectangle│      │ Circle│
 │........ │ .... │ ......│
 │+Name    │      │+Name  │
 │+Area()  │      │+Area()│
 └─────────┘      └───────┘

这样我们就能用同一个接口操作任何Shape的子类,但每个子类“底下”怎么实现都随意。

抽象方法和属性的UML图:


┌────────────────────────────┐
│        abstract class      │
│           Animal           │
│────────────────────────────│
│+ Name: string {abstract}   │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
              │
     ┌────────┴──────────┐
     │                   │
┌────────────┐     ┌─────────────┐
│    Cat     │     │    Dog      │
│────────────│     │─────────────│
│+ Name      │     │+ Name       │
│+ MakeSound()│    │+ MakeSound()│
└────────────┘     └─────────────┘

4. 实际场景和对项目的好处

抽象方法和属性是你设计可扩展类体系的利器。它们是大型业务应用、多层领域模型、插件系统、UI框架的基石——为系统的“地基”定下“规矩”,所有子类都得遵守。

在真实项目里,这种“纯净”的架构让你过了几个月、几年还能放心加新功能,因为老代码已经考虑到了各种行为。

“为啥要用”以及回归多态

多态看起来像魔法,其实就是个契约:“你可以对任何家族对象调用某个方法,总能得到合适的结果”。抽象方法和属性就是让整个继承体系都说同一种“语言”,但又不强制默认实现。

这种方式在插件系统(IDE、图形编辑器、CRM系统)里用得超多,第三方开发者写扩展时,必须实现平台要求的最小功能集。比如你写个文件处理模块,基类可以有抽象属性FileExtension和抽象方法Open(),这样任何插件都能正确处理自己类型的文件。

抽象、虚方法和普通成员的区别

特性 普通方法/属性 virtual abstract
有无实现 没有
必须override吗 不用 不用(可以重写) 必须(子类里必须实现)
能不能直接调用 不能
能不能在普通类里 不能
能不能标记sealed 不能 不能

5. 在咱们的学习应用里怎么用

现在有了这些新知识,回到我们的图形应用,看看抽象属性和方法怎么一起让系统又灵活又清晰。


// 比如在主程序里
List<Shape> shapes = new List<Shape>
{
    new Rectangle(4, 5),
    new Circle(2.5)
};

foreach (Shape shape in shapes)
{
    // 多亏了抽象属性Name和方法CalculateArea,
    // 我们不用关心具体是什么图形
    Console.WriteLine($"{shape.Name}, 面积: {shape.CalculateArea():F2}");
}

输出:

矩形, 面积: 20.00
圆, 面积: 19.63

美滋滋:代码根本不用知道具体是哪种图形——只管调用属性和方法,.NET CLR会自动用对的实现!

6. 常见错误和实现细节

错误1:没实现父类的抽象方法。
如果子类有一个抽象方法或属性没实现,而且类本身也没标记成abstract,编译器就不让你过。这其实是保护你:你不能创建一个“有漏洞”的对象,比如没有CalculateArea()方法的图形。

错误2:方法声明成abstract,但类不是抽象的。
这种代码也编译不过。你要是加了abstract方法,类本身也得是抽象的:


public abstract class Polygon : Shape
{
    // 没实现CalculateArea(),类必须abstract
}

错误3:想用virtual实现抽象方法。
抽象方法必须用override来实现,不能用virtual。不过你要是想让后面的子类还能重写,可以先override,然后在子类里再声明成virtual


public override double CalculateArea()
{
    // 默认实现...
}

更深一层的子类就能再重写这个方法。但不能再把它声明成abstract,因为已经有实现了。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION