virtual 和 abstract
1. virtual
和 abstract
关键字如何使用
在 C# 中,virtual
和 abstract
关键字通常用于基类方法、属性、事件等的声明,以便允许派生类在必要时对其进行重写。两者的主要区别在于:
virtual
关键字声明的成员在基类中有一个默认实现,派生类可以选择重写它。abstract
关键字声明的成员没有实现,必须在派生类中实现。
1.1virtual
关键字
virtual
关键字用于定义可以在派生类中重写的成员。它为成员提供了一个默认实现,派生类可以选择重写,也可以不重写。
使用 virtual
的方法
public class Animal
{
// 使用 virtual 声明一个可以被重写的方法
public virtual void MakeSound()
{
Console.WriteLine("Animal makes sound");
}
}
public class Dog : Animal
{
// 重写基类的 MakeSound 方法
public override void MakeSound()
{
Console.WriteLine("Dog barks");
}
}
public class Cat : Animal
{
// 不重写基类的 MakeSound 方法,使用基类的实现
}
在这个例子中:
Animal
类定义了一个MakeSound
方法,并使用virtual
关键字。Dog
类重写了MakeSound
方法,提供了自己的实现。Cat
类没有重写MakeSound
,所以会使用基类Animal
的默认实现。
virtual
关键字的应用场景
当基类提供一个默认行为,但允许派生类根据需要修改时。
提供一种可扩展的结构,使得子类能够根据具体需求定制行为。
1.2abstract
关键字
abstract
关键字用于定义没有具体实现的方法、属性等。包含 abstract
成员的类本身必须也是抽象类(即用 abstract
修饰的类),不能直接实例化。派生类必须实现所有继承的 abstract
成员。
使用 abstract
的方法
// 抽象类
public abstract class Animal
{
// 抽象方法,没有实现,必须在派生类中实现
public abstract void MakeSound();
}
public class Dog : Animal
{
// 必须重写 MakeSound 方法,因为它在基类中是抽象的
public override void MakeSound()
{
Console.WriteLine("Dog barks");
}
}
public class Cat : Animal
{
// 必须重写 MakeSound 方法
public override void MakeSound()
{
Console.WriteLine("Cat meows");
}
}
在这个例子中:
Animal
类是抽象类,定义了一个抽象方法MakeSound
。Dog
和Cat
类必须实现MakeSound
方法,因为它是抽象的,没有默认实现。
abstract
关键字的应用场景
当基类不提供默认行为,但要求所有派生类都必须提供自己的实现时。
设计强制子类实现某些核心功能,确保派生类都有特定的行为。
在 C# 中,override
关键字用于重写从基类继承的方法、属性、事件或索引器。通过使用 override
,派生类可以提供该成员的自定义实现,而不是使用基类中的实现。为了重写基类中的方法,基类中的成员必须使用 virtual
或 abstract
关键字进行声明。
1.3override
用法
虚方法 (
virtual
) 与重写 (override
) 方法基类中的方法使用
virtual
声明,派生类可以通过override
关键字来重写它:// 基类 public class Animal { public virtual void MakeSound() { Console.WriteLine("Animal makes sound"); } } // 派生类 public class Dog : Animal { public override void MakeSound() { Console.WriteLine("Dog barks"); } }
在上面的例子中,
Dog
类重写了基类Animal
中的MakeSound
方法。抽象方法 (
abstract
) 与重写 (override
) 方法抽象类中的抽象方法必须在派生类中使用
override
进行重写。抽象方法没有方法体,必须在派生类中实现。// 抽象基类 public abstract class Animal { public abstract void MakeSound(); } // 派生类 public class Cat : Animal { public override void MakeSound() { Console.WriteLine("Cat meows"); } }
这里的
MakeSound
方法在基类Animal
中是抽象的,必须在Cat
类中实现。重写属性
与方法类似,属性也可以被重写:
public class Animal { public virtual string Name { get; set; } = "Animal"; } public class Dog : Animal { public override string Name { get; set; } = "Dog"; }
调用基类的实现
在重写的同时可以选择调用基类中的实现:
public class Animal { public virtual void MakeSound() { Console.WriteLine("Animal makes sound"); } } public class Dog : Animal { public override void MakeSound() { base.MakeSound(); // 调用基类的实现 Console.WriteLine("Dog barks"); } }
在这个例子中,
Dog
类不仅执行了自己对MakeSound
的实现,还调用了基类Animal
的MakeSound
方法。
1.4 总结
virtual
:用于定义可选的重写功能,基类提供默认实现,派生类可以选择重写。abstract
:用于定义必须实现的功能,基类没有实现,派生类必须提供具体实现。不能重写没有使用
virtual
或abstract
声明的方法。override
方法必须与被重写的方法具有相同的签名(方法名、参数类型及返回类型必须一致)。如果一个类中的成员被重写了,使用该类的实例时,将调用派生类的实现,而不是基类的实现。
2. 既然使用abstract,为什么不直接使用virtual?
2.1 对比
abstract
和 virtual
都允许派生类重写基类中的方法,但它们的使用场景和意义有所不同:
virtual
:基类提供了一个 默认实现,派生类可以选择接受该实现,也可以选择重写它。virtual
适用于基类能提供合理默认行为的情况。如果派生类不重写,基类的实现将被使用。abstract
:基类 不提供实现,并强制派生类必须实现这个方法。适用于基类无法提供合理默认行为的情况,或者基类的设计要求所有派生类必须提供特定的行为。virtual
示例(基类提供默认实现):public class Animal { public virtual void MakeSound() { Console.WriteLine("Animal makes sound"); } } public class Dog : Animal { // 可选择重写或使用默认实现 public override void MakeSound() { Console.WriteLine("Dog barks"); } }
abstract
示例(强制派生类实现):public abstract class Animal { public abstract void MakeSound(); // 基类无法提供实现 } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("Dog barks"); } } public class Cat : Animal { public override void MakeSound() { Console.WriteLine("Cat meows"); } }
2.2 总结
当基类 无法提供合理的默认行为 或者设计上需要每个派生类 必须提供自己的实现 时,应该使用
abstract
。当基类可以提供一个 合理的默认实现,但允许派生类根据需要重写时,使用
virtual
。
3. abstract类和interface有什么区别
3.1 对比
方法实现:
抽象类:可以包含方法的 部分实现,也可以有抽象方法,派生类必须实现这些抽象方法。
接口:不能包含任何具体的实现(在早期版本的 C# 中),所有方法都是没有实现的,必须由实现类提供定义。在 C# 8.0 及之后的版本,接口可以包含
default
实现,但这仍然是少数情况下使用的特性。
// 抽象类可以有方法实现 public abstract class Shape { public abstract double GetArea(); // 抽象方法 public void Show() // 普通方法 { Console.WriteLine("Showing shape"); } } // 接口中只能有方法签名 public interface IShape { double GetArea(); // 没有实现 }
继承机制:
抽象类:只能继承一个抽象类(因为 C# 中不支持多继承),但可以实现多个接口。抽象类可以包含字段、属性、方法、事件等各种成员。
接口:类可以实现多个接口,这使得接口比抽象类更灵活。接口只定义行为(方法、属性、事件等),没有状态(成员变量或字段)。
public class Dog : Animal, IAnimalBehavior, IComparable { // 实现接口的所有成员 }
成员访问修饰符:
抽象类:可以有不同的访问修饰符(
public
,protected
,private
等),控制成员的可见性。接口:接口中的所有成员默认都是 public,不能有访问修饰符。
字段(成员变量):
抽象类:可以包含字段和常量。
接口:不能包含任何字段,只能包含方法、属性、事件等。
public abstract class Animal { protected string name; // 抽象类可以有字段 } public interface IAnimal { // 接口不能有字段 }
使用场景:
抽象类:用于表示一组有紧密关系的对象,并可以为这些对象提供部分默认行为。它适合有共同特性且需要部分实现的类。适合类层次结构中,提供共享的基础功能。
接口:用于表示无关的对象可以实现相同的行为。接口定义了行为契约,而不关心具体实现。适合不相关类之间的功能共享。
3.2 示例对比
抽象类:
public abstract class Animal { public string Name { get; set; } public abstract void MakeSound(); // 抽象方法 public void Sleep() // 普通方法 { Console.WriteLine("Animal is sleeping"); } }
接口:
public interface IAnimal { void MakeSound(); // 接口方法 }
3.3 如何选择
使用抽象类:如果多个类之间存在共享的代码或状态,且需要提供某种基础实现。
使用接口:如果希望强制一组类实现某些行为(例如多个不相关的类可以实现相同的接口),且不需要共享代码。
3.4 总结
抽象类:可以包含方法实现、字段,适合类层次结构中共享功能。
接口:不包含具体实现,类可以实现多个接口,适合不相关的类共享行为。