C# - virtual (虚拟)、override (重写)、new 修饰符

创建时间:
2016-03-17 23:59
最近更新:
2018-10-26 11:51

override 的 译文

Resource - MSDN

  1. virtual (C# 参考) - virtual 关键字用于修饰方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。
  2. override (C# 参考) - 要扩展或修改继承的方法、属性、索引器或事件的抽象实现或虚实现,必须使用 override 修饰符。The override modifier is required to extend or modify the abstract or virtual implementation of an inherited method, property, indexer, or event.
  3. new 修饰符 (C# 参考)
  4. 使用 Override 和 New 关键字进行版本控制 (C# 编程指南)
  5. 了解何时使用 Override 和 New 关键字 (C# 编程指南)
  6. 如何: 重写 ToString 方法 (C# 编程指南)

"隐式隐藏" 与 "使用 new 关键字显式隐藏"

以下基类与派生类中声明了两个签名相同的方法:

class A {
    public void F(){}
}
class A : B {
    public void F(){}
}

编译器在编译程序时会发出警告 (仍可通过编译),并建议用 new 关键字隐藏基类相同签名的方法。
注意,使用 new 关键字,隐藏仍会发生,它为唯一的作用就是关闭警告。事实上,new 关键字的意思就是说 "我知道自己在干什么,不要再警告我了。"
new 方法好比新方法,表明这个方法区别于基类中的同签名方法。也就说,在 C# 中派生类如果定义一个和基类中相同签名的方法会自动隐藏,但是会警告。
相反,Java 中完全没有这种语法、没有这种限制,因为 Java 中所有方法都是 "默认 virtual" 的。Java 中定义和父类完全相同的方法时,叫做 "重写",子类会自动隐藏父类中相同签名方法。如果想调用父类中相同签名方法就用 super 来调用。

隐藏方法的特点:

  • 必须位于基类和派生类中;
  • 方法名称必须相同;
  • 参数类型、参数个数必须相同;
  • 返回值类型可以不同;
  • 应该使用 new,虽然不使用 new 也会运行,但在编译时会被警告;
  • 调用派生类方法还是基类方法,取决于被什么类型的实例调用。

Virtual Method (虚方法) - virtual & override

只有在声明时使用了 virtual 关键字修饰的方法,才能被派生类 override,这样的方法称为 虚方法 (Virtual Method)。

在派生类中,若要 override 基类的虚方法,必须要遵守下列几个规则:

  • 必须要使用 override 关键字声明方法。
  • 方法的名称、参数与返回值类型必须和父类中声明的一样。
  • 方法的访问权限也必须和父类中声明的一样。

在 C# 中,方法在默认情况下不是 virtual 的,但可以显式地声明为 virtual。此规则符合标准 OOP,与 C++相同,除非显式指定,否则方法就不是 virtual 的,而 Java 中所有方法默认都是虚 (virtual) 方法且不需要使用任何关键字就可以重写父类中的方法。

构造函数不能声明为 virtual。
成员字段不能声明为 virtual。
静态函数不能声明为 virtual。

将方法声明为 virtual 会导致性能略微受损。

在基类中提供的虚方法版本 (virtual) 只是以防万一,因为有可能某个派生类没有实现自己从基类继承而来的虚方法,那么它就可以放心大胆的去调用基类中实现。

override 目的是提供 virtual 方法的不同实现,这些方法是相互关联的,因为它们旨在完成相同的任务,只是不同的类会以不同的方式去完成。
隐藏方法 (使用或不用 new) 的目的是将一个方法替换成另一个方法,方法通常是互不关联,而且可能执行完全不同的任务。

Visual Studio 的智能感知能在敲出 override 关键字后提示出基类中所有虚方法。

Example - 重写虚方法

最最常用的 virtual 方法是 public virtual string System.Object.ToString(),以下是它的重写示例:

public class A
{
    private object _field;

    public A(object value) {
        _field = value;
    }

    public override string ToString() {
        string s = string.Format(
            "{0} & {1}",
            base.ToString(), // 使用 base 调用基类中的实现
            _field.ToString()
        );
        return s;
    }
}

在 C# 中使用 virtual 和 override 关键字的规则

  1. 不允许使用 private 或者 override 关键字来声明一个 virtual 方法,否则会得到一个编译时错误。private 意味着只有类自身才能访问,但是 virtual 意味着可以在派生中重写它,因此矛盾。override 意味着这个方法是从基类中继承而来,与 virtual 相矛盾。
  2. virtual 方法与 override 方法的签名必须完全一致,连返回值类型也必须相同。
  3. virtual 方法与 override 方法必须具有相同的可访问性。
  4. 只能 override 声明为 virtual 的方法,假如基类中方法不是 virtual 的,但是你试图 override 它,就会得到一个编译时错误。这个设计是合理的:应该由基类的设计者决定一个方法是否 override 它。但是在 Java 中所有方法默认都是虚方法,所以可以随时重写一个方法。
  5. 假如派生类不用 override 关键字来声明方法,就不会覆盖基类方法。换言之,它会成为和基类的方法完全不相关的另一个实现,该方法只是恰巧和基类完全同名。而且还会造成一个警告。可是使用 new 关键字消除警告。这和 Java 完全不一样,Java 中虚方法为默认,而且重写一个方法也不需要任何关键字。
  6. 一个 override 方法将隐式的成为 virtual 方法,本身可以在未来的一个派生类中被 override,然而,不允许使用 virtual 关键字将一个 override 方法显式声明为 virtual 方法。

虚方法 (virtual) 和多态性

虚方法 (virtual) 允许调用同一个方法的不同版本,取决于运行时动态确定的对象类型。
只有虚方法 (virtual 才能实现多态),基类引用指向不同派生类。对于非虚方法的同名方法,基类引用只会调用基类中的同名方法,不会调用派生类中的同名方法。这和 Java 则完全不一样,Java 中没有 "同名方法" 这概念,所以父类引用指向子类对象,只会调用子类中那个版本方法实现,如果子类没有实现,则调用父类实现。

重写 (override) 的特点

  • 必须位于基类和派生类中;
  • 方法名称必须相同;
  • 参数类型、参数个数必须相同;
  • 返回值类型必须相同 (与隐藏不同);
  • 必须使用关键字 virtual 和 override;
  • 即使把派生类的实例转换成基类类型,也无法调用基类中被覆盖的方法,因为它已经被覆盖,不像隐藏还有被 "发掘" 的机会。

new 与 override 的关键区别 重点

class A
{
    public virtual void F () { }
}
class B : A
{
    public override void F () { }
}
class C : A
{
    public new void F () { }
}
class Test
{
    static void Main ()
    {
        new B() .F(); //调用派生类中的实现。
    ((A)new B()).F(); //调用派生类中的实现。
        new C() .F(); //调用派生类中的实现。
    ((A)new C()).F(); //调用基类中的实现。
    }
}

字段与属性 的 隐藏 (new) 与 重写 (override)

属性可以用 隐藏 (new),也可以用 重写 (override)。
隐藏 (new) 的话,基类与派生类属性的类型可以不同;
重写 (override) 的话,基类与派生类属性的类型必须相同。

属性类似于方法而非字段,因为属性和方法一样是逻辑代码,而字段是数据代码,因此属性可以用 隐藏 (new) 和 重写 (override);而字段只能用 隐藏 (new),不能用 重写 (override)。

扩展: 属性也可以用 abstract (字段不行),abstract 的属性,也是 override 的。

测试记录 - new

using System;

// 只要派生类中的成员与基类成员同名 (数据类型可不相同)
// 使用 new 关键字修饰符显式隐藏从基类继承的成员。
// 虽然可以在不使用 new 修饰符的情况下隐藏基类成员,但会生成警告;如果使用 new 显式隐藏成员,则会取消此警告。
public class A
{
    public int _X = 3;
    public string _Y = "a";
    public int _Z = 9;
}

public class B : A
{
    // 下面对基类字段的重写,其结果相当于基类与派生类各有一个字段,虽然这 2 个字段名相同,但完全是 2 个不相干的字段:
    new public static int _X = 6;
    // 不使用 new 关键字,与上面使用 new 关键字,除编译时会提示之外,无区别:
    public static string _Y = "b"; // 无论是 int 还是 string,都会隐藏,且引用不会混淆。
    public decimal _Z = 99; // 即使基类为 int 而此处为 decimal、且未使用 new 关键字,也会隐藏基类的字段。

    static void Main ()
    {
        A a = new A();
        B b = new B();

        Console.WriteLine(_X);
        Console.WriteLine(a._X);

        Console.WriteLine(_Y);
        Console.WriteLine(a._Y);

        Console.WriteLine(b._Z);
        Console.WriteLine(a._Z);
    }
}

测试记录 - 并非必须实现基类中的 virtual 方法

using System;
namespace Net451Console
{
    class A
    {
        protected virtual void F() { }
    }

    class Program : A
    {
        static void Main(string[] args)
        {
            Console.WriteLine("本示例可正常运行。说明并非必须实现基类中的 virtual 方法。");
        }
    }
}