菜鸟的C#学习(二)
文章目录
- 一、类的访问
- 1、普通类继承抽象类
- 2、普通类继承抽象类,抽象类继承接口,三者联系
- 二、类中方法的访问
- 2.1 抽象方法和虚方法
- 2.2 虚方法和普通方法
- **1. 调用机制**
- **2. 方法重写**
- **3. 设计意图**
- **4. 性能差异**
- **5. 语法对比表**
- **总结:何时使用?**
一、类的访问
1、普通类继承抽象类
在C#里,普通类继承抽象类时,有以下这些要点需要留意:
1. 必须实现所有抽象成员
抽象类中的抽象方法和属性不具备实现代码,继承它的普通类得把这些抽象成员全部实现
出来。实现时,方法签名要和抽象类中定义的保持一致,并且要用override
关键字。
public abstract class Shape
{public abstract double Area(); // 抽象方法
}public class Circle : Shape
{public double Radius { get; set; }// 实现抽象方法public override double Area() => Math.PI * Radius * Radius;
}
2. 遵循访问修饰符的限制
在实现抽象成员时,访问修饰符要和抽象类中定义的一样。比如,抽象类里的抽象方法是protected
,那么派生类中实现该方法时也得用protected
。
3. 不能直接实例化抽象类
抽象类没办法直接创建实例,必须通过派生类来实例化。
Shape shape = new Circle { Radius = 5 }; // 正确
Shape shape = new Shape(); // 错误,无法实例化抽象类
4. 可以添加新成员
继承抽象类的普通类能够新增自己的字段、属性、方法或者事件。
public class Rectangle : Shape
{public double Width { get; set; }public double Height { get; set; }public override double Area() => Width * Height;// 新增方法public double Perimeter() => 2 * (Width + Height);
}
5. 抽象类可以包含非抽象成员
抽象类中除了抽象成员,还能有已经实现的方法、属性等,派生类可以直接继承或者重写这些非抽象成员。
public abstract class Animal
{public string Name { get; set; }public void Eat() => Console.WriteLine($"{Name} is eating."); // 非抽象方法public abstract void MakeSound(); // 抽象方法
}public class Dog : Animal
{public override void MakeSound() => Console.WriteLine("Woof!");
}
6. 抽象类也能继承自其他类或抽象类
如果抽象类继承了另一个抽象类,它可以选择实现部分抽象成员,剩下的由派生类去实现。
public abstract class Vehicle
{public abstract void Start();
}public abstract class Car : Vehicle
{public override void Start() => Console.WriteLine("Car started."); // 实现基类的抽象方法public abstract void Drive(); // 定义新的抽象方法
}public class SportsCar : Car
{public override void Drive() => Console.WriteLine("Sports car is driving fast.");
}
7. 不能用 sealed 修饰派生类
因为普通类要实现抽象类的抽象成员,所以不能用sealed
关键字修饰该普通类,不然就没办法被其他类继承了。
总结
普通类继承抽象类时,要实现所有抽象成员,遵循访问修饰符的规则,不能实例化抽象类,不过可以添加新成员。抽象类可以有非抽象成员,还能继承其他类或抽象类。
2、普通类继承抽象类,抽象类继承接口,三者联系
当一个类(派生类)继承抽象基类,而抽象基类又实现了接口时,三者的成员函数关系遵循以下规则(以C#为例):
1. 接口定义“契约”,抽象基类部分或全部实现,派生类完成剩余实现
- 接口:定义必须实现的成员(如方法、属性),但不提供实现。
- 抽象基类:
- 必须“声明”实现接口的所有成员(即使只实现部分)。
- 可将部分接口成员标记为
abstract
(延迟到派生类实现),其他成员提供具体实现。
- 派生类:
- 必须实现抽象基类中标记为
abstract
的接口成员(若有)。 - 可选择重写(
override
)抽象基类中已实现的接口成员(若为virtual
)。
- 必须实现抽象基类中标记为
2. 示例说明
假设存在以下结构:
// 接口定义
public interface IMyInterface
{void MethodA(); // 接口方法void MethodB();
}// 抽象基类实现接口
public abstract class MyAbstractBase : IMyInterface
{public void MethodA() // 具体实现接口方法{Console.WriteLine("Base.MethodA");}public abstract void MethodB(); // 抽象方法,延迟到派生类实现
}// 派生类继承抽象基类
public class MyDerivedClass : MyAbstractBase
{public override void MethodB() // 实现抽象基类的抽象方法{Console.WriteLine("Derived.MethodB");}
}
成员关系分析:
- 接口
IMyInterface
:定义MethodA()
和MethodB()
。 - 抽象基类
MyAbstractBase
:- 实现
MethodA()
,派生类可直接使用。 - 将
MethodB()
标记为abstract
,强制派生类实现。
- 实现
- 派生类
MyDerivedClass
:- 无需关心
MethodA()
(已由基类实现)。 - 必须实现
MethodB()
,否则会编译错误。
- 无需关心
3. 特殊情况:抽象基类未完全实现接口
若抽象基类未实现接口的所有成员(即部分接口成员未被标记为 abstract
且未提供实现),则会导致编译错误。例如:
public abstract class MyAbstractBase : IMyInterface
{// 错误:未实现 MethodB(),且未声明为 abstractpublic void MethodA() { }
}
修正方式:
- 将
MethodB()
声明为abstract
(如示例所示)。 - 或在抽象基类中提供
MethodB()
的具体实现。
4. 接口显式实现与隐式实现
抽象基类可选择显式实现接口(只能通过接口类型调用):
public abstract class MyAbstractBase : IMyInterface
{void IMyInterface.MethodA() // 显式实现接口方法{Console.WriteLine("Explicit implementation");}public abstract void MethodB();
}
此时,派生类需通过接口类型调用 MethodA()
:
MyDerivedClass derived = new MyDerivedClass();
((IMyInterface)derived).MethodA(); // 必须转型为接口类型
5. 派生类重写基类的实现
若抽象基类的方法为 virtual
,派生类可选择重写:
public abstract class MyAbstractBase : IMyInterface
{public virtual void MethodA() { } // 虚拟方法public abstract void MethodB();
}public class MyDerivedClass : MyAbstractBase
{public override void MethodA() { } // 重写基类方法public override void MethodB() { } // 实现抽象方法
}
6. 多层继承的扩展
若存在多层继承(如抽象基类继承自另一个抽象基类),规则相同:
- 每个抽象基类可实现部分接口成员,剩余抽象成员由最终派生类实现。
- 示例:
public interface IMyInterface { void MethodA(); } public abstract class Base1 : IMyInterface { public abstract void MethodA(); } public abstract class Base2 : Base1 { } // 未实现 MethodA(),仍为抽象类 public class Derived : Base2 { public override void MethodA() { } } // 最终实现
总结
角色 | 对接口成员的责任 | 对抽象成员的责任 |
---|---|---|
接口 | 定义所有成员签名 | 无 |
抽象基类 | 必须声明实现所有接口成员(部分或全部实现) | 可定义抽象成员,强制派生类实现 |
派生类 | 实现抽象基类中未实现的接口成员(即抽象成员) | 必须实现基类的所有抽象成员 |
这种分层设计允许:
- 接口 定义统一契约。
- 抽象基类 复用通用逻辑,简化派生类实现。
- 派生类 专注于核心差异化逻辑。
二、类中方法的访问
2.1 抽象方法和虚方法
在C#中,抽象方法和虚方法都用于实现多态性,但它们的设计目的和使用方式有本质区别。以下是两者的核心差异:
1. 定义语法与强制实现
抽象方法 | 虚方法 |
---|---|
使用 abstract 关键字声明,且不能有方法体。csharp<br>public abstract void Print();<br> | 使用 virtual 关键字声明,必须有默认实现。csharp<br>public virtual void Print() { Console.WriteLine("Base"); }<br> |
必须由派生类实现,否则派生类必须声明为抽象类。 | 派生类可以选择是否重写,不重写时将继承基类的默认实现。 |
2. 所在类的限制
- 抽象方法:只能存在于抽象类中(即使用
abstract
修饰的类)。 - 虚方法:可以存在于普通类或抽象类中。
3. 重写要求
抽象方法 | 虚方法 |
---|---|
派生类必须使用 override 关键字实现,且不能使用 new 或 sealed 隐藏基类方法。 | 派生类使用 override 关键字重写(推荐),或使用 new 关键字隐藏基类方法(不推荐)。 |
示例:csharp<br>public override void Print() { ... }<br> | 示例:csharp<br>public override void Print() { ... } // 重写<br>public new void Print() { ... } // 隐藏(不推荐)<br> |
4. 设计目的
- 抽象方法:用于定义必须由子类实现的契约,基类只规定方法签名,不提供默认行为。例如:
public abstract class Shape {public abstract double Area(); // 所有形状必须计算面积 }
- 虚方法:用于提供可扩展的默认行为,允许子类在需要时修改实现。例如:
public class Animal {public virtual void Speak() { Console.WriteLine("Animal sound"); } }public class Dog : Animal {public override void Speak() { Console.WriteLine("Woof"); } // 可选重写 }
5. 调用方式
- 抽象方法:无法直接调用,必须通过派生类的实现调用。
- 虚方法:可以直接通过基类调用默认实现,也可以通过派生类调用重写后的实现。
总结对比表
特性 | 抽象方法 | 虚方法 |
---|---|---|
方法体 | 不能有方法体 | 必须有默认实现 |
所在类 | 必须在抽象类中 | 可以在普通类或抽象类中 |
强制实现 | 派生类必须实现 | 派生类可选重写 |
关键字 | abstract + override | virtual + override (可选) |
设计意图 | 定义必须实现的契约 | 提供可扩展的默认行为 |
示例代码
// 抽象类 + 抽象方法
public abstract class Vehicle
{public abstract void Start(); // 必须由子类实现
}// 普通类 + 虚方法
public class Car : Vehicle
{public override void Start() { Console.WriteLine("Car started"); } // 实现抽象方法public virtual void Drive() { Console.WriteLine("Driving normally"); } // 虚方法,提供默认行为
}// 派生类重写虚方法
public class SportsCar : Car
{public override void Drive() { Console.WriteLine("Driving fast!"); } // 重写虚方法
}
何时使用?
- 使用抽象方法:当基类无法提供有意义的默认实现,且所有子类必须强制实现某个行为时。
- 使用虚方法:当基类可以提供默认行为,但子类可能需要自定义实现时。
通过合理使用抽象方法和虚方法,可以构建出灵活且易于扩展的面向对象系统。
2.2 虚方法和普通方法
在C#中,虚方法(virtual
)和普通方法(无修饰符)的核心区别在于是否支持运行时多态。以下是两者的详细对比:
1. 调用机制
虚方法 | 普通方法 |
---|---|
使用 virtual 关键字声明,支持运行时多态。基类的虚方法可以在派生类中被override 重写。调用时,会根据对象的实际类型决定执行哪个版本的方法。 | 没有特殊修饰符,不支持运行时多态。调用时,根据对象的声明类型决定执行的方法,无论对象的实际类型是什么。 |
示例:csharp<br>public class Animal {<br> public virtual void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public override void Speak() { Console.WriteLine("Dog"); }<br>}<br><br>// 输出:Dog<br>Animal animal = new Dog();<br>animal.Speak(); // 调用Dog的实现<br> | 示例:csharp<br>public class Animal {<br> public void Speak() { Console.WriteLine("Animal"); }<br>}<br><br>public class Dog : Animal {<br> public new void Speak() { Console.WriteLine("Dog"); } // 使用new隐藏基类方法(不推荐)<br>}<br><br>// 输出:Animal<br>Animal animal = new Dog();<br>animal.Speak(); // 调用Animal的实现<br> |
2. 方法重写
虚方法 | 普通方法 |
---|---|
可以被派生类使用 override 关键字重写,从而改变方法的行为。 | 不能被重写,但可以使用 new 关键字隐藏基类方法(但这不是真正的重写,只是创建了一个同名的新方法)。 |
正确做法:csharp<br>public class Base {<br> public virtual void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public override void Print() { ... } // 重写虚方法<br>}<br> | 错误做法(隐藏而非重写):csharp<br>public class Base {<br> public void Print() { ... }<br>}<br><br>public class Derived : Base {<br> public new void Print() { ... } // 隐藏基类方法(编译警告)<br>}<br> |
3. 设计意图
虚方法 | 普通方法 |
---|---|
用于实现多态性,允许基类定义通用行为,派生类根据需要自定义实现。例如:csharp<br>public class Shape {<br> public virtual double Area() => 0;<br>}<br><br>public class Circle : Shape {<br> public override double Area() => Math.PI * Radius * Radius;<br>}<br> | 用于实现固定行为,不希望派生类修改方法逻辑。例如:csharp<br>public class Calculator {<br> public int Add(int a, int b) => a + b; // 不需要重写的方法<br>}<br> |
4. 性能差异
- 虚方法:调用时需要通过虚函数表(VTable)动态查找实际要执行的方法,因此性能略低(但在大多数场景下可以忽略不计)。
- 普通方法:调用时直接绑定到声明类型的方法,性能更高。
5. 语法对比表
特性 | 虚方法 | 普通方法 |
---|---|---|
关键字 | virtual | 无 |
能否重写 | 能(使用 override ) | 不能(只能用 new 隐藏) |
多态支持 | 运行时多态(根据对象实际类型) | 编译时绑定(根据声明类型) |
默认行为 | 基类提供默认实现,可被覆盖 | 行为固定,不可被派生类修改 |
性能 | 略低(通过VTable查找) | 更高(直接调用) |
总结:何时使用?
- 使用虚方法:
- 当基类希望派生类能够自定义某个方法的实现时。
- 需要通过基类引用调用派生类方法(实现多态)。
- 使用普通方法:
- 当方法的逻辑不需要被派生类修改时。
- 性能敏感的场景(如高频调用的方法)。
通过合理使用虚方法和普通方法,可以在保证代码灵活性的同时,避免不必要的性能开销。