virtual 和 abstract

1. virtualabstract 关键字如何使用

在 C# 中,virtualabstract 关键字通常用于基类方法、属性、事件等的声明,以便允许派生类在必要时对其进行重写。两者的主要区别在于:

  • 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. 当基类提供一个默认行为,但允许派生类根据需要修改时。

  2. 提供一种可扩展的结构,使得子类能够根据具体需求定制行为。

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

  • DogCat 类必须实现 MakeSound 方法,因为它是抽象的,没有默认实现。

abstract 关键字的应用场景

  1. 当基类不提供默认行为,但要求所有派生类都必须提供自己的实现时。

  2. 设计强制子类实现某些核心功能,确保派生类都有特定的行为。

在 C# 中,override 关键字用于重写从基类继承的方法、属性、事件或索引器。通过使用 override,派生类可以提供该成员的自定义实现,而不是使用基类中的实现。为了重写基类中的方法,基类中的成员必须使用 virtualabstract 关键字进行声明。

1.3override 用法

  1. 虚方法 (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 方法。

  2. 抽象方法 (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 类中实现。

  3. 重写属性

    与方法类似,属性也可以被重写:

     public class Animal
     {
         public virtual string Name { get; set; } = "Animal";
     }
     ​
     public class Dog : Animal
     {
         public override string Name { get; set; } = "Dog";
     }
  4. 调用基类的实现

    在重写的同时可以选择调用基类中的实现:

     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 的实现,还调用了基类 AnimalMakeSound 方法。

1.4 总结

  • virtual:用于定义可选的重写功能,基类提供默认实现,派生类可以选择重写。

  • abstract:用于定义必须实现的功能,基类没有实现,派生类必须提供具体实现。

  • 不能重写没有使用 virtualabstract 声明的方法。

  • override 方法必须与被重写的方法具有相同的签名(方法名、参数类型及返回类型必须一致)。

  • 如果一个类中的成员被重写了,使用该类的实例时,将调用派生类的实现,而不是基类的实现。


2. 既然使用abstract,为什么不直接使用virtual?

2.1 对比

abstractvirtual 都允许派生类重写基类中的方法,但它们的使用场景和意义有所不同:

  • 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 对比

  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();  // 没有实现
    }
  2. 继承机制:

    • 抽象类:只能继承一个抽象类(因为 C# 中不支持多继承),但可以实现多个接口。抽象类可以包含字段、属性、方法、事件等各种成员。

    • 接口:类可以实现多个接口,这使得接口比抽象类更灵活。接口只定义行为(方法、属性、事件等),没有状态(成员变量或字段)。

    public class Dog : Animal, IAnimalBehavior, IComparable
    {
        // 实现接口的所有成员
    }
  3. 成员访问修饰符:

    • 抽象类:可以有不同的访问修饰符(public, protected, private 等),控制成员的可见性。

    • 接口:接口中的所有成员默认都是 public,不能有访问修饰符。

  4. 字段(成员变量):

    • 抽象类:可以包含字段和常量。

    • 接口:不能包含任何字段,只能包含方法、属性、事件等。

    public abstract class Animal
    {
        protected string name; // 抽象类可以有字段
    }
    
    public interface IAnimal
    {
        // 接口不能有字段
    }
  5. 使用场景:

    • 抽象类:用于表示一组有紧密关系的对象,并可以为这些对象提供部分默认行为。它适合有共同特性且需要部分实现的类。适合类层次结构中,提供共享的基础功能。

    • 接口:用于表示无关的对象可以实现相同的行为。接口定义了行为契约,而不关心具体实现。适合不相关类之间的功能共享。

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 总结

  • 抽象类:可以包含方法实现、字段,适合类层次结构中共享功能。

  • 接口:不包含具体实现,类可以实现多个接口,适合不相关的类共享行为。


virtual 和 abstract
http://localhost:8090/archives/virtual-abstract
作者
Administrator
发布于
2024年10月15日
许可协议