虚拟、重写、新建和密封重写的区别

83
我对面向对象编程(OOP)的几个概念感到困惑,包括"virtual", "override", "new"和"sealed override"。有人可以解释一下它们之间的区别吗?
我很清楚,如果要使用派生类方法,可以使用"override"关键字覆盖基类方法。但是我不确定"new"和"sealed override"的作用。
4个回答

112
virtual关键字用于修改方法、属性、索引器或事件声明,并允许在派生类中进行重写。例如,任何继承它的类都可以重写这个方法: 使用new修饰符来明确隐藏从基类继承的成员。要隐藏一个继承的成员,在派生类中使用相同的名称声明它,并用new修饰符进行修改。
这与多态性有关。当在引用上调用虚方法时,使用引用所指向的对象的实际类型来决定使用哪个方法实现。当在派生类中重写基类的方法时,即使调用代码不“知道”对象是派生类的实例,也会使用派生类中的版本。例如:
public class Base {
    public virtual void SomeMethod() {
    }
}

public class Derived : Base {
    public override void SomeMethod() {
    }
}

...

Base d = new Derived();
d.SomeMethod();

如果Derived类重写了Base类的SomeMethod方法,那么最终会调用Derived类的SomeMethod方法。

现在,如果你使用new关键字而不是override,那么派生类中的方法并不会重写基类中的方法,而只是隐藏它。在这种情况下,代码可能会像这样:

public class Base {
    public virtual void SomeOtherMethod() {
    }
}

public class Derived : Base {
    public new void SomeOtherMethod() {
    }
}

...


Base b = new Derived();
Derived d = new Derived();
b.SomeOtherMethod();
d.SomeOtherMethod();

首先调用Base.SomeOtherMethod,然后调用Derived.SomeOtherMethod。它们实际上是两个完全独立的方法,只是恰好具有相同的名称,而不是派生方法覆盖基方法。
如果您既不指定new也不指定overrides,那么结果输出与指定new相同,但您还将收到编译器警告(因为您可能不知道您隐藏了基类方法中的一个方法,或者您可能希望覆盖它,只是忘记包含关键字)。
覆盖属性声明可以包括sealed修饰符。使用此修饰符可以防止派生类进一步覆盖该属性。封闭属性的访问器也是封闭的。

谢谢您的输入。但有一件事我不明白,Base b = new Derived() 的用途是什么?这是创建基类对象还是派生类对象? - xorpower
2
派生类。我认为你需要更深入地了解多态性。这里有一个很好的阅读材料。http://msdn.microsoft.com/en-us/library/ms173152(v=vs.80).aspx - CharithJ
5
在这种情况下,您正在创建一个 Derived 对象的实例,并将引用存储在 Base 变量中。这是有效的,因为 Derived 对象也是 Base 对象。就像说我们需要一个“人”,所以我们得到了恰好是一个人的“Johnny”。这里也是同样的道理。 - Jeff Mercado
我想在这里补充一点。Base b = new Derived() 表示一个 Base 类可以通过 Derived 类的引用进行访问,因为 Derived 类是其基类的特化。Derived 类可以执行所有操作(例如调用基类方法等),而 Base 类可以执行的操作则不能由其 Derived 类执行。因此,Derived d = new Base() 是不正确的,但 Base b = new Derived() 是正确的。 - mmushtaq
你能澄清使用new修饰符来隐藏基类方法的目的吗?在第二个例子中,调用b.SomeOtherMethod()会调用基类实现(可以说它已经“隐藏”了派生类的方法)。如果这是一个典型的用法示例,那么当调用者打算使用编译时类型的变量来使用其方法而不是任何可能分配给它的运行时类型的方法时,似乎会使用new - Minh Tran
为了后人留存:SO:C#中的运行时类型与编译时类型 - Minh Tran

37
任何方法都可以被覆盖(使用virtual关键字)或者不被覆盖。决定权在定义方法的人手中。
class Person
{
    // this one is not overridable (not virtual)
    public String GetPersonType()
    {
        return "person";
    }
    
    // this one is overridable (virtual)
    public virtual String GetName()
    {
        return "generic name";
    }
}

现在你可以重写那些可重写的方法了。
class Friend : Person
{
    public Friend() : this("generic name") { }
    
    public Friend(String name)
    {
        this._name = name;
    }
    
    // override Person.GetName:
    public override String GetName()
    {
        return _name;
    }
}

但是你不能覆盖GetPersonType方法,因为它不是虚方法。
让我们创建这些类的两个实例:
Person person = new Person();
Friend friend = new Friend("Onotole");

当非虚拟方法GetPersonTypeFriend实例调用时,实际上调用的是Person.GetPersonType
Console.WriteLine(friend.GetPersonType()); // "person"

当通过Friend实例调用虚方法GetName时,实际调用的是Friend.GetName
Console.WriteLine(friend.GetName()); // "Onotole"

当通过Person实例调用虚拟方法GetName时,调用的是Person.GetName
Console.WriteLine(person.GetName()); // "generic name"

当调用非虚方法时,不会查找方法体 - 编译器已经知道需要调用的实际方法。而对于虚方法,编译器无法确定要调用哪个方法,它会在运行时从下到上在类层次结构中进行查找,从方法被调用的实例类型开始:对于friend.GetName,它从Friend类开始查找,并立即找到;对于person.GetName,它从Person类开始查找,并在那里找到。
有时候你会创建一个子类,重写一个虚方法,并且不希望在层次结构中再有其他重写 - 你可以使用sealed override来实现这一点(表示你是最后一个重写该方法的人):
class Mike : Friend
{
    public sealed override String GetName()
    {
        return "Mike";
    }
}

但有时候你的朋友Mike决定改变性别,从而改变名字为Alice :) 你可以选择修改原始代码,或者选择继承Mike的子类。
class Alice : Mike
{
    public new String GetName()
    {
        return "Alice";
    }
}

在这里,您创建了一个完全不同的方法,但名称相同(现在您有两个方法)。哪个方法在什么时候被调用?这取决于您如何调用它:
Alice alice = new Alice();
Console.WriteLine(alice.GetName());             // the new method is called, printing "Alice"
Console.WriteLine(((Mike)alice).GetName());     // the method hidden by new is called, printing "Mike"

当你从Alice的角度来调用它时,你调用Alice.GetName,当从Mike的角度来调用它时,你调用Mike.GetName。这里没有进行运行时查找,因为这两个方法都是非虚方法。
无论你隐藏的方法是虚方法还是非虚方法,你都可以随时创建新的方法。
这也适用于属性和事件 - 它们在底层都表示为方法。

1
我在任何地方都没有找到比这更简单和完整的答案。谢谢Loki。 - Rajeev Menon

22
默认情况下,除非在派生类中声明为virtualabstract,否则方法无法在派生类中被重写。 virtual表示“调用前检查新的实现”,而abstract表示“相同”,但保证在所有派生类中被重写。此外,在基类中不需要实现它,因为它将在其他地方重新定义。
以上规则有一个例外,即使用new修饰符。未声明为virtualabstract的方法可以在派生类中使用new修饰符重新定义。当在基类中调用该方法时,执行基本方法,而在派生类中调用时,执行新的方法。 new关键字允许您在类层次结构中拥有两个名称相同的方法。
最后,sealed修饰符打破了virtual方法的链,使它们不能再次被覆盖。这不经常使用,但选项存在。它在三个类的链中派生自上一个类时更有意义。
A -> B -> C

如果在类 A 中含有一个被类 B 重写的 virtual 或者 abstract 方法,那么可以通过在类 B 中将其声明为 sealed 来防止其他类如 C 再次改变它。

sealed 关键字同样用于 class 类中,这是你最常见到这个关键字的地方。

希望能对你有所帮助。


8
 public class Base 
 {
   public virtual void SomeMethod()
   {
     Console.WriteLine("B");
   }
  }

public class Derived : Base
{
   //Same method is written 3 times with different keywords to explain different behaviors. 


   //This one is Simple method
  public void SomeMethod()
  {
     Console.WriteLine("D");
  }

  //This method has 'new' keyword
  public new void SomeMethod()
  {
     Console.WriteLine("D");
  }

  //This method has 'override' keyword
  public override void SomeMethod()
  {
     Console.WriteLine("D");
  }
}

现在首先要做的是,

开始


 Base b=new Base();
 Derived d=new Derived();
 b.SomeMethod(); //will always write B
 d.SomeMethod(); //will always write D

现在,关键词都与多态性有关。

 Base b = new Derived();
  1. 在基类中使用virtual,并在Derived中使用override可以实现D(多态)。
  2. Base中使用override而没有virtual会导致错误。
  3. 类似地,编写一个带有virtual的方法(不覆盖)将带来警告'B'(因为未进行任何多态操作)。
  4. 为了隐藏以上情况中的警告,在Derived中的简单方法之前添加new
  5. new关键字是另一种故事,它只是隐藏了警告,告诉我们在基类中存在相同名称的属性。
  6. virtualnew除了new修饰符之外是相同的

  7. newoverride不能在同一方法或属性前使用。

  8. sealed在任何类或方法之前锁定它以在派生类中使用,并且会产生编译时错误。

抱歉,由于多个编译错误,得分为-1:方法使用相同参数多次声明,字符串B和D周围没有引号... - DeveloperDan

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