Наследование и полиморфизм
Операции is и as
При преобразованиях ссылочных типов используются операции преобразования is и as. Вот код, иллюстрирующий особенности применения операций is и as:
using System; namespace derivation01 { // Базовый класс... class X {//_____________________________. public int f1(int key) { Console.WriteLine("X.f1"); return key; } }//_____________________________. // Производный... class Y:X {//_____________________________. new public int f1(int key) { Console.WriteLine("Y.f1"); base.f1(key); return key; } public int yf1(int key) { Console.WriteLine("Y.yf1"); return key; } }//_____________________________. // Производный... class Z:X {//_____________________________. public int zf1(int key) { Console.WriteLine("Z.zf1"); return key; } }//_____________________________. class Class1 { static void Main(string[] args) { int i; // Всего лишь ссылки на объекты... X x; Y y; Z z; Random rnd = new Random(); // И вот такой тестовый пример позволяет выявить особенности применения // операций is и as. for (i = 0; i < 10; i++) { // Ссылка на объект базового класса случайным образом // настраивается на объекты производного класса. if (rnd.Next(0,2) == 1) x = new Y(); else x = new Z(); // Вызов метода f1 не вызывает проблем. // Метод базового класса имеется у каждого объекта. x.f1(0); // А вот вызвать метод, объявленный в производном классе // (с использованием операции явного приведения типа), // удается не всегда. Метод yf1 был объявлен лишь в классе Y. // Ниже операция is принимает значение true лишь в том случае, // если ссылка на объект базового класса была настроена на объект // класса Y. if (x is Y) { ((Y)x).yf1 (0); // И только в этом случае можно вызвать метод, // объявленный в Y. } else { ((Z)x).zf1 (1); // А в противном случае попытка вызова yf1 // привела бы к катастрофе. try { ((Y)x).yf1 (0); } catch (Exception ex) { Console.WriteLine("–1–" + ex.ToString()); } } // А теперь объект, адресуемый ссылкой на базовый класс, надо попытаться // ПРАВИЛЬНО переадресовать на ссылку соответствующего типа. И это тоже // удается сделать не всегда. Явное приведение может вызвать исключение. try { z = (Z)x; } catch (Exception ex) { Console.WriteLine("–2–" + ex.ToString()); } try { y = (Y)x; } catch (Exception ex) { Console.WriteLine("–3–" + ex.ToString()); } // Здесь помогает операция as. // В случае невозможности переадресации соответствующая ссылка оказывается // установленной в null. А эту проверку выполнить проще... if (rnd.Next(0,2) == 1) { z = x as Z; if (z != null) z.zf1(2); else Console.WriteLine("?????"); } else { y = x as Y; if (y != null) y.yf1(3); else Console.WriteLine("!!!!!"); } } } } }Листинг 7.5.
Boxing и Unboxing. Приведение к типу object
Любой тип .NET строится на основе базового типа (наследует) object. Это означает, что:
- методы базового типа object доступны к выполнению любым (производным) типом,
- любой объект (независимо от типа) может быть преобразован к типу object и обратно. Деятельность по приведению объекта к типу object называется Boxing. Обратное преобразование называют Unboxing.
Примеры вызова методов базового класса:
int i = 125; i.ToString(); i.Equals(100);
Примеры преобразований:
int i = 125; // Объявление и инициализация переменной типа int. object o = i; // Неявное приведение к типу object в результате присвоения. object q = (object)i; // Явное приведение к типу object. int x = (int)o; // Пример Unboxing'а.
И еще пример кода на ту же тему:
using System; namespace Boxing02 { class TestClass { public int t; public string str; // Конструктор класса принимает параметры в упакованном виде. public TestClass(object oT, object oStr) { t = (int)oT; // unboxing посредством явного преобразования. str = oStr as string; // unboxing в исполнении операции as. } } class Program { static void Main(string[] args) { TestClass tc; // При создании объекта пакуем параметры. tc = new TestClass((object)0, (object)(((int)0).ToString())); Console.WriteLine("tc.t == {0}, tc.str == {1}", tc.t, tc.str); } } }
Виртуальные функции. Принцип полиморфизма
Основа реализации принципа полиморфизма – наследование. Ссылка на объект базового класса, настроенная на объект производного, может обеспечить выполнение методов ПРОИЗВОДНОГО класса, которые НЕ БЫЛИ ОБЪЯВЛЕНЫ В БАЗОВОМ КЛАССЕ. При реализации принципа полиморфизма происходят вещи, которые не укладываются в ранее описанную схему.
Одноименные функции с одинаковой сигнатурой могут объявляться как в базовом, так и в производном классах. Между этими функциями может быть установлено отношение замещения:
функция ПРОИЗВОДНОГО класса ЗАМЕЩАЕТ функцию БАЗОВОГО класса.
Основное условие отношения замещения заключается в том, что замещаемая функция базового класса должна при этом дополнительно специфицироваться спецификатором virtual.
Отношение замещения между функциями базового и производного класса устанавливается, если соответствующая (одноименная функция с соответствующей сигнатурой) функция производного класса специфицируется дополнительным спецификатором override.
Введем следующие обозначения и построим схему наследования.
public void F(){ } | 0 | |
public virtual void F() { } | 0 | |
new public virtual void F() { } | 1..N | |
new public void F() { } | 1..N | |
public override void F() { } | 1..N |
Схема возможных вариантов объявления методов в иерархии наследования трех уровней:
Наконец, замечательный пример:
using System; namespace Implementing { class A { public virtual void F() { Console.WriteLine("A");} } class B:A { public override void F() { Console.WriteLine("B"); } } class C:B { new public virtual void F() { Console.WriteLine("C"); } } class D:C { public override void F() { Console.WriteLine("D"); } } class Starter { static void Main(string[] args) { D d = new D(); C c = d; B b = c; A a = b; d.F(); /* D */ c.F(); /* D */ b.F(); /* B */ a.F(); /* B */ } } }Листинг 7.6.
в котором становится ВСЕ понятно, если ПОДРОБНО нарисовать схемы классов, структуру объекта — представителя класса D и посмотреть, КАКАЯ ссылка ЗА КАКОЕ место этот самый объект удерживает.