C#中虚函数的实际用途

60

在 C# 中,虚函数的实际用途是什么?

10个回答

91

基本上,如果在您的祖先类中,您希望某个方法具有某种行为。如果您的后代使用相同的方法但具有不同的实现,则可以通过加上virtual关键字来进行覆盖

using System;
class TestClass 
{
   public class Dimensions 
   {
      public const double pi = Math.PI;
      protected double x, y;
      public Dimensions() 
      {
      }
      public Dimensions (double x, double y) 
      {
         this.x = x;
         this.y = y;
      }

      public virtual double Area() 
      {
         return x*y;
      }
   }

   public class Circle: Dimensions 
   {
      public Circle(double r): base(r, 0) 
      {
      }

      public override double Area() 
      { 
         return pi * x * x; 
      }
   }

   class Sphere: Dimensions 
   {
      public Sphere(double r): base(r, 0) 
      {
      }

      public override double Area()
      {
         return 4 * pi * x * x; 
      }
   }

   class Cylinder: Dimensions 
   {
      public Cylinder(double r, double h): base(r, h) 
      {
      }

      public override double Area() 
      {
         return 2*pi*x*x + 2*pi*x*y; 
      }
   }

   public static void Main()  
   {
      double r = 3.0, h = 5.0;
      Dimensions c = new Circle(r);
      Dimensions s = new Sphere(r);
      Dimensions l = new Cylinder(r, h);
      // Display results:
      Console.WriteLine("Area of Circle   = {0:F2}", c.Area());
      Console.WriteLine("Area of Sphere   = {0:F2}", s.Area());
      Console.WriteLine("Area of Cylinder = {0:F2}", l.Area());
   }
}

编辑: 问题在评论中提出
如果我在基类中不使用virtual关键字,会起作用吗?

如果您在派生类中使用override关键字,则不会起作用。您将生成编译器错误CS0506'function1':无法覆盖继承的成员'function2',因为它未标记为“virtual”、“abstract”或“override”。

如果您没有使用override,则会收到警告CS0108 'desc.Method()'隐藏了继承的成员'base.Method()'。如果打算进行隐藏,请使用new关键字。

要解决此问题,请在您正在隐藏的方法前面放置new关键字。

例如:

  new public double Area() 
  {
     return 2*pi*x*x + 2*pi*x*y; 
  }

..是否强制在派生类中重写虚函数?
不是的,如果你不重写该方法,则派生类将使用继承自基类的方法。


3
如果我在基类中不使用虚拟关键字会怎样?它会工作吗?在派生类中重写虚拟方法是强制性的吗? - Preeti
4
@Pre 我已在评论中回答了你的问题。 - Johnno Nolan

77

理解虚函数的实际用法的关键在于记住,一个特定类的对象可以被分配给另一个从第一个对象的类派生的类的对象。

例如:

class Animal {
   public void eat() {...}
}

class FlyingAnimal : Animal {
   public void eat() {...}
}

Animal a = new FlyingAnimal();

Animal类有一个eat()函数,通常描述动物应该如何进食(例如将对象放入嘴中并吞咽)。

然而,FlyingAnimal类应定义一个新的eat()方法,因为飞行动物有一种特殊的进食方式。

所以这里出现的问题是:当我声明类型为Animal的变量a并将其分配给类型为FlyingAnimal的新对象后,a.eat()会做什么?哪个方法被调用了?

答案是:因为a的类型是Animal,它将调用Animal的方法。编译器很蠢,不知道您将要向a变量分配另一个类的对象。

这就是virtual关键字发挥作用的地方:如果你将方法声明为virtual void eat() {...},你基本上在告诉编译器“小心,我在这里做了一些聪明的事情,你无法处理,因为你不够聪明”。因此,编译器不会尝试将调用a.eat()链接到两个方法中的任何一个,而是告诉系统在运行时去执行!

因此,只有在代码执行时,系统才会查看a实际类型而不是其声明类型,并执行FlyingAnimal的方法。

你可能会想:为什么我要这样做?为什么不一开始就说FlyingAnimal a = new FlyingAnimal()

这样做的原因是,例如,您可能有许多从Animal派生出来的类:FlyingAnimalSwimmingAnimalBigAnimalWhiteDog等。然后在某个时候,您想定义一个包含许多Animal的世界,所以您说:

Animal[] happy_friends = new Animal[100];

我们有一个有100只快乐动物的世界。 在某个时刻,您对它们进行初始化:

...
happy_friends[2] = new AngryFish();
...
happy_friends[10] = new LoudSnake();
...

到了一天的尾声,你希望每个人在睡前都吃饱。因此,你想说:

for (int i=0; i<100; i++) {
   happy_friends[i].eat();
}

正如你所看到的,每种动物都有自己的进食方法。只有使用虚函数才能实现这种功能。否则,每个人都将被迫以最常见的Animal类中描述的方式进行“进食”。

编辑: 在像Java这样的通用高级语言中,这个行为实际上是默认的。


8
为什么不将Animal变成一个接口呢?因为Animal包含了所有动物都能使用的基本功能。 - Djorge
3
这是我迄今为止看过的最好的“虚拟”解释。感谢你使它如此直观易懂。 - software_writer
1
一个特定类的对象可以被分配给另一个对象 -- 我想你的意思是一个变量可以被分配为一个对象(不可能将对象“1”分配给对象“2”)。 - Alexey

7

就像其他编程语言一样,当你需要使用多态时,它可以有很多用途。例如,你想要抽象化从控制台、文件或其他设备读取输入的方式,你可以使用一个通用的读取器接口,然后使用虚函数来实现多个具体的读取器。


4
例如代理方法,即在运行时重写方法。例如,NHibernate使用此功能来支持延迟加载。

4
这样可以实现晚期绑定,也就是在运行时而不是编译时确定要调用哪个对象的成员。详见维基百科

我也一直认为这是virtual关键字的目的,但是这个答案解释了我们的错误。 - Ivan Ruski

3

虚拟成员基本上允许您表达多态性,派生类可以具有与其基类中的方法相同签名的方法,基类将调用派生类的方法。

一个基本示例:

public class Shape
{
    // A few example members
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }

    // Virtual method
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

3
“and the base class will call the derived class's method.” 这句话与示例代码一起使用会令人困惑;文本表明base.Draw将调用派生类的Draw方法(这可能导致StackOverflowException异常)。我理解您的意思,但新手可能不会。不过,这个示例代码本身非常好;运行它将清楚地显示发生了什么。 - Fredrik Mörk

1
例如,您有一个基类Params和一组派生类。您希望能够对存储所有可能从params派生的类的数组执行相同的操作。
没问题-声明方法为虚拟的,在Params类中添加一些基本实现并在派生类中覆盖它。现在,您只需遍历数组并通过引用调用该方法-将调用正确的方法。
class Params {
public:
   virtual void Manipulate() { //basic impl here }
}

class DerivedParams1 : public Params {
public:
   override void Manipulate() {
      base.Manipulate();
      // other statements here
   }
};

// more derived classes can do the same

void ManipulateAll( Params[] params )
{
    for( int i = 0; i < params.Length; i++ ) {
       params[i].Manipulate();
    }
 }

1

C#中虚函数的使用

虚函数主要用于在派生类中以相同的签名重写基类方法。

当派生类继承基类时,派生类对象是对派生类或基类的引用。

虚函数由编译器进行迟绑定(即运行时绑定)。

在基类中使用 virtual 关键字后,根据所引用对象的实际类型而非指针或引用的声明类型,调用最终派生类的函数实现。如果没有使用 virtual ,则方法会通过早期绑定解析,并根据指针或引用的声明类型选择要调用的函数。


1

这里开始:

在面向对象编程中,虚函数或虚方法是一种函数或方法,其行为可以被一个继承类中具有相同签名的函数所覆盖。


1

例子

让我们考虑 System.Object 中的 ToString() 方法。由于这个方法是 System.Object 的成员,它会被所有类继承,并为它们提供 ToString() 方法。

namespace VirtualMembersArticle
{
    public class Company
    {
        public string Name { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Company company = new Company() { Name = "Microsoft" };
            Console.WriteLine($"{company.ToString()}");
            Console.ReadLine();
        }   
    }
}

上段代码的输出为:
VirtualMembersArticle.Company

假设我们想要改变Company类中从System.Object继承的ToString()方法的标准行为。为了达到这个目标,只需要使用override关键字声明该方法的另一个实现即可。

public class Company
{
    ...
    public override string ToString()
    {
        return $"Name: {this.Name}";
    }         
}

现在,当一个虚方法被调用时,运行时将检查其派生类中是否有覆盖成员,并在存在时调用它。我们应用程序的输出如下:
Name: Microsoft

事实上,如果您查看System.Object类,您会发现该方法被标记为虚拟的。
namespace System
{
    [NullableContextAttribute(2)]
    public class Object
    {
        ....
        public virtual string? ToString();
        ....
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接