Я считаю, что всем знакомы три особенности объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм. Каждый может сказать пару слов, но большинство знает лишь, что это такое, а не то, как CLR реализует их внутри. Поэтому в этой статье в основном рассматриваются некоторые концепции полиморфизма и внутренний механизм реализации.

1. Понятие полиморфизма

Сначала объясним, что такое полиморфизм: одна и та же операция, применяемая к разным объектам, может интерпретироваться по-разному и давать разные результаты выполнения. Это и есть полиморфизм. Иными словами, экземпляр одного и того же типа вызывает «тот же самый» метод, но результаты оказываются разными. Слово «тот же самый» здесь в кавычках, потому что методы, которые выглядят одинаково, на самом деле являются разными вызываемыми методами.

Когда речь заходит о полиморфизме, нельзя не упомянуть следующие понятия: перегрузка, переопределение, виртуальные методы, абстрактные методы и сокрытие методов. Давайте разберём эти понятия по очереди.

1, ПерегрузкаВ одной и той же области видимости (обычно в классе) два или более метода с одинаковым именем, но разными списками параметров называются перегрузкой. У них есть три характеристики (обычно называемые «два должны и один может»):

  • Имена методов должны совпадать
  • Списки параметров должны различаться
  • Типы возвращаемых значений могут различаться

Например:

public void Sleep()
{
    Console.WriteLine("Animal睡觉");
}
public int Sleep(int time)
{
    Console.WriteLine("Animal{0}点睡觉", time);
    return time;
}

2, ПереопределениеВ подклассе, чтобы удовлетворить собственные потребности, переопределение другой реализации метода требует ключевого слова override. Переопределяемый метод должен быть виртуальным методом, для чего используется ключевое слово virtual. Его характеристики таковы (три одинаковых):

  • Одинаковое имя метода
  • Одинаковый список параметров
  • Одинаковый тип возвращаемого значения

Например: определение в родительском классе:

public virtual void EatFood()
{
    Console.WriteLine("Animal吃东西");
}

Определение в дочернем классе:

public override void EatFood()
{
    Console.WriteLine("Cat吃东西");
    //base.EatFood();
}

Совет:Часто некоторые друзья спрашивают о разнице между перегрузкой и переопределением, и в интернете это один из распространённых вопросов на собеседовании по C#. На самом деле эти два понятия вообще не связаны; они лишь оба содержат символ "over". Нет смысла их сравнивать. Просто различайте их разные определения.

3, Виртуальный методМетод, определённый в базовом классе, который допускает переопределение в производных классах, определяется с помощью ключевого слова virtual. Например:

public virtual void EatFood()
{
    Console.WriteLine("Animal吃东西");
}

ПримечаниеВиртуальные методы также можно вызывать напрямую. Например:

Animal a = new Animal();
a.EatFood();

Вывод:

Animal吃东西

4, Абстрактный методМетод, определённый в базовом классе и который должен быть переопределён в производных классах. Он определяется с помощьюabstractключевого слова. Например:

public abstract class Biology
{
    public abstract void Live();
}
public class Animal : Biology
{
    public override void Live()
    {
        Console.WriteLine("Animal重写的抽象方法");
        //throw new NotImplementedException();
    } 
}

Примечание:Абстрактные методы можно определять только в абстрактных классах. Если они определены не в абстрактном классе, сообщается следующая ошибка:

Разница между виртуальными методами и абстрактными методами состоит в следующем:Поскольку абстрактные классы нельзя инстанцировать, абстрактные методы нельзя вызывать; другими словами, абстрактные методы никогда не могут быть реализованы.

5, Скрывающий методМетод, определённый в производном классе с тем же именем, что и метод в базовом классе, определяется с помощью ключевого слова new. Например, в базовом классе Animal есть метод Sleep():

public void Sleep()
{
    Console.WriteLine("Animal Sleep");
}

Тогда код для определения скрывающего метода в производном классе Cat выглядит так:

new public void Sleep()
{
    Console.WriteLine("Cat Sleep");
}

Или:

public new void Sleep()
{
    Console.WriteLine("Cat Sleep");
} 

Примечание:

  • (1) Скрывающий метод может скрывать не только виртуальные методы в базовом классе, но и невиртуальные методы в базовом классе.
  • (2) Для скрывающего метода экземпляр родительского класса вызывает метод родительского класса, а экземпляр дочернего класса вызывает метод дочернего класса.
  • (3) По сравнению с предыдущим пунктом: для переопределяющего метода переменная дочернего класса вызывает переопределённый метод дочернего класса. Для переменной родительского класса это зависит от того, указывает ли ссылка родительского класса на экземпляр дочернего класса или на его собственный экземпляр. Если она ссылается на экземпляр родительского класса, вызывается метод базового класса; если она ссылается на экземпляр производного класса, вызывается метод производного класса.

Итак, основные понятия рассмотрены. Давайте посмотрим на пример. Сначала мы создаём несколько классов:

public abstract class Biology
{
    public abstract void Live();
}
public class Animal : Biology
{
    public override void Live()
    {
        Console.WriteLine("Animal重写的Live");
        //throw new NotImplementedException();
    }
    public void Sleep()
    {
        Console.WriteLine("Animal Sleep");
    }
    public int Sleep(int time)
    {
        Console.WriteLine("Animal在{0}点Sleep", time);
        return time;
    }
    public virtual void EatFood()
    {
        Console.WriteLine("Animal EatFood");
    }
}
public class Cat : Animal
{
    public override void EatFood()
    {
        Console.WriteLine("Cat EatFood");
        //base.EatFood();
    }
    new public void Sleep()
    {
        Console.WriteLine("Cat Sleep");
    }
    //public new void Sleep()
    //{
    //    Console.WriteLine("Cat Sleep");
    //}
}
public class Dog : Animal
{
    public override void EatFood()
    {
        Console.WriteLine("Dog EatFood");
        //base.EatFood();
    }
}

Теперь посмотрим на код, который нужно выполнить:

class Program
{
    static void Main(string[] args)
    {
        //Animal的实例
        Animal a = new Animal();
        //Animal的实例,引用派生类Cat对象
        Animal ac = new Cat();
        //Animal的实例,引用派生类Dog对象
        Animal ad = new Dog();
        //Cat的实例
        Cat c = new Cat();
        //Dog的实例
        Dog d = new Dog();
        //重载
        a.Sleep();
        a.Sleep(23);
        //重写和虚方法
        a.EatFood();
        ac.EatFood();
        ad.EatFood();
        //抽象方法
        a.Live();
        //隐藏方法
        a.Sleep();
        ac.Sleep();
        c.Sleep();
        Console.ReadKey();
    }
}

Сначала мы определяем несколько экземпляров классов, которые нам нужно использовать. Следует отметить:

  • (1) Класс Biology является абстрактным классом, и из него нельзя создать экземпляр;
  • (2) Переменная ac является экземпляром Animal, но указывает на объект Cat. Поскольку Cat является производным классом Animal, это преобразование допустимо. Это также ключевой момент полиморфизма.

Далее давайте разберём шаг за шагом:

1,

//重载
a.Sleep();
a.Sleep(23);

Очевидно, два метода Sleep, вызываемые переменной a класса Animal, являются перегруженными методами. Первый оператор вызывает метод Sleep() без параметров, а второй оператор вызывает метод Sleep с параметром int. Обратите внимание, что типы возвращаемого значения двух методов Sleep различаются, что также иллюстрирует последнюю из трёх характеристик переопределения — тип возвращаемого значения может отличаться.

Результат выполнения выглядит следующим образом:

Animal Sleep
Animal在23点Sleep

2,

//重写和虚方法
a.EatFood();
ac.EatFood();
ad.EatFood();

В этом разделе a, ac и ad — все являются экземплярами Animal, но ссылаются на разные объекты. a ссылается на объект Animal, ac ссылается на объект Cat, а ad ссылается на объект Dog. Как это повлияет на результаты выполнения? Смотрите результаты выполнения ниже:

Animal EatFood
Cat EatFood
Dog EatFood

Первый оператор: экземпляр Animal напрямую вызывает виртуальный метод EatFood класса Animal — никаких проблем.

Во втором и третьем операторах, хотя они также являются экземплярами Animal, они указывают соответственно на объекты Cat и Dog, поэтому вызываются методы EatFood, переопределённые в классах Cat и Dog, так же как если бы экземпляры Cat и Dog напрямую вызывали EatFood. Это воплощение полиморфизма: одна и та же операция, применённая к разным объектам, может иметь разную интерпретацию и давать разные результаты выполнения.

3,

//抽象方法
a.Live();

Это относительно просто. Он напрямую переопределяет метод Live родительского класса Biology. Результат выполнения выглядит следующим образом:

Animal重写的Live

4,

//隐藏方法
a.Sleep();
ac.Sleep();
c.Sleep();

При анализе метода сокрытия его следует сравнивать с виртуальными методами и переопределением. Нет возражений против того, что переменная a вызывает метод Sleep класса Animal, а переменная c — метод Sleep класса Cat. Но переменная ac ссылается на объект типа Cat. Должна ли она вызывать метод EatFood типа Animal или метод EatFood типа Cat? Ответ: она вызывает метод родительского класса, а именно метод EatFood класса Animal. Результат выполнения выглядит следующим образом:

Animal Sleep
Animal Sleep
Cat Sleep

Большинство статей останавливаются на этом, лишь знакомя нас с этими понятиями и вызовом методов, не объясняя, почему это так. Далее давайте немного углубимся и поговорим о механизме, лежащем в основе полиморфизма.


2. Углублённое понимание полиморфизма

Чтобы глубоко понять полиморфизм, нужно начать с типов значений и ссылочных типов. Мы все знаем, что типы значений хранятся в стеке потока, а ссылочные типы — в управляемой куче. Поскольку все классы являются ссылочными типами, мы рассматриваем только ссылочные типы.

Теперь вернёмся к предыдущему примеру. Main — это точка входа в программу. Когда JIT-компилятор компилирует функцию Main в машинные команды CPU, он обнаруживает, что этот метод ссылается на классы Biology, Animal, Cat и Dog, поэтому CLR создаёт несколько экземпляров, представляющих сами эти типы. Мы называем их «объектами типов». Этот объект содержит статические поля класса, таблицу методов, содержащую все методы класса, а также два дополнительных члена, которые должны быть у всех объектов в управляемой куче, — указатель на объект типа (Type Object Pointer) и индекс блока синхронизации (Sync Block Index).

Приведённый выше отрывок может быть непонятен некоторым друзьям, которые не читали соответствующие книги по CLR, поэтому давайте нарисуем схему, описывающую это:

Приведённая выше схема показывает, что делает CLR перед выполнением функции Main. Далее начинается выполнение функции Main (для удобства упростим функцию Main):

//Animal的实例
Animal a = new Animal();
//Animal的实例,引用派生类Cat对象
Animal ac = new Cat();
//Animal的实例,引用派生类Dog对象
Animal ad = new Dog();
a.Sleep();
a.EatFood();
ac.EatFood();
ad.EatFood();

Теперь создадим три экземпляра Animal, но фактически они указывают соответственно на объект Animal, объект Cat и объект Dog, как показано ниже:

Обратите внимание: хотя переменные ac и ad обе имеют тип Animal, они указывают соответственно на объект Cat и объект Dog. Это ключевой момент.

При выполнении a.Sleep(), поскольку Sleep — это невиртуальный метод экземпляра, JIT-компилятор найдёт объект типа (объект типа Animal), соответствующий типу (Animal) переменной (a), через которую сделан вызов. Затем он вызывает метод Sleep в этом объекте типа. Если в этом объекте типа нет метода Sleep, JIT-компилятор проходит обратно по базовым классам класса (вплоть до Object), чтобы найти метод Sleep.

При выполнении ac.EatFood, поскольку EatFood является виртуальным методом экземпляра, JIT-компилятор при вызове генерирует в методе некоторый дополнительный код. Этот код сначала проверяет переменную (ac), из которой был сделан вызов, затем по ссылочному адресу переменной находит объект, из которого был сделан вызов (объект Cat), находит объект типа, соответствующий объекту, из которого был сделан вызов (объект типа Cat), и наконец ищет метод EatFood в этом объекте типа. Аналогично, если метод EatFood не найден в этом объекте типа, JIT-компилятор возвращается по иерархии к базовому классу этого объекта типа, чтобы найти его.

Описанное выше — это различные способы выполнения JIT-компилятором вызовов невиртуальных методов экземпляра и виртуальных методов экземпляра. Именно эта разная обработка двух типов методов порождает одну из трёх особенностей объектно-ориентированного программирования, которую мы видим на поверхности, — полиморфизм.

Исходный URL: https://www.cnblogs.com/zhangkai2237/archive/2012/12/20/2826734.html