Конспект лекций (C#)-unlocked. 1 Основные сведения о C# Особенности языка
Скачать 1.97 Mb.
|
не происходит. Аналогично, может осуществляться сокрытие членов-данных, например: class Class1 { public int a; } class Class2 : Class1 { public new double a; } Если произошло сокрытие члена базового класса, то доступ к нему все же мо- жет быть осуществлён с использованием ключевого слова base , например: class Class1 { public int a = 1; public void Mul() { a *= 2; 73 } } class Class2 : Class1 { public new double a; public new void Mul() { a *= 3; } public void Calc() { base.Mul(); // Class1.a = 2 a = base.a; // Class2.a = 2.0 Mul(); // Class2.a = 6.0; } } 4.3 Полиморфизм Язык С# является строго типизированным языком. Поэтому, строго говоря, переменная ссылочного типа может ссылаться только на объект этого типа. Однако из этого правила есть одно важное исключение: переменной-ссылке на объект ба- зового класса может быть присвоена ссылка на объект любого производного от него класса. Такое присваивание считается вполне допустимым, поскольку экзем- пляр объекта производного класса наследует все члены базового класса. Однако, до- ступа к членам, имеющимся только в производном классе, в этом случае не будет. Пример: Figure figure = new Square(); string s = figure.sType; // s = "Квадрат" // У базового класса нет свойства size, поэтому следующая строка // не допустима // double d = figure.size; Возможность присвоения переменной-ссылке на базовый класс ссылки на производный класс может быть использована, например, при реализации конструк- торов копии. Пример: изменение конструктора копии класса Square с применением вызо- ва конструктора копии базового класса. public Square(Square sourceSquare) : base(sourceSquare) { _size = sourceSquare._size; } Возможность переменной ссылки на базовый класс хранить объекты любого производного класса и правильно выполнять набор операций, определённых в базо- вом классе, и называется полиморфизмом. 74 Именно из-за этой возможности часто создаются обобщённые базовые классы, на основе которых строятся производные классы с конкретными особенностями ре- ализации. Пример: перестроим иерархию классов, добавив в неё класс «прямоугольник» и разместив метод расчёта площади в базовом классе. class Figure { protected string _sType; protected int _posX, _posY; public string sType { get { return _sType; } } public int posX { get { return _posX; } set { if (value >= 0) _posX = value; } } public int posY { get { return _posY; } set { if (value >= 0) _posY = value; } } public Figure() : this("Не задан",1,1) { } public Figure(string n_sType) : this(n_sType,1,1) { } public Figure(string n_sType, int n_posX, int n_posY) { _sType = n_sType.Trim() != "" ? n_sType.Trim() : "Не задан"; _posX = n_posX >= 0 ? n_posX : 1; _posY = n_posY >= 0 ? n_posY : 1; } public Figure(Figure sourceFigure) { _sType = sourceFigure._sType; _posX = sourceFigure._posX; _posY = sourceFigure._posY; } public Figure Duplicate() { return new Figure(this); } public double Area() { return 0; // Метод должен что-то возвращать } } 75 class Square : Figure { private double _size; public double size { get { return _size; } set { if (value > 0) _size = value; } } public Square() : base("Квадрат") { _size = 1; } public Square(Square sourceSquare) : base(sourceSquare) { _size = sourceSquare._size; } new public Square Duplicate() { return new Square(this); } new public double Area() { return _size*_size; } } class Rectangle : Figure { private double _size1, _size2; public double size1 { get { return _size1; } set { if (value > 0) _size1 = value; } } public double size2 { get { return _size2; } set { if (value > 0) _size2 = value; } } public Rectangle() : base("Прямоугольник") { _size1 = _size2 = 2; // Просто для различия площади } public Rectangle(Rectangle sourceRectangle) : base(sourceRectangle) { _size1 = sourceRectangle._size1; _size2 = sourceRectangle._size2; } new public Rectangle Duplicate() { return new Rectangle(this); } 76 new public double Area() { return _size1*_size2; } } Figure f = new Figure(); string s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Не задан. Площадь: 0,00" f = new Square(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Квадрат. Площадь: 0,00" f = new Rectangle(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Прямоугольник. Площадь: 0,00" Результаты оказались неожиданными. Несмотря на то, что в каждой из строк правильно выведен тип фигуры, расчёт площади выполнен не верно. Это происхо- дит из-за того, что в момент компиляции производится связь между данными объек- та и методами класса, к которому принадлежит переменная – раннее связывание. Кроме того, если в программу добавить строку Rectangle r = f.Duplicate(); то данная строка не будет скомпилирована, т.к. будет использоваться метод Duplicate класса Figure из-за того, что переменная f имеет данный тип, а при- своение переменной-ссылке на объект-родитель ссылки на объект-потомок недопу- стимо. Для устранения недостатка по расчёту площади используются виртуальные методы. Устранение недостатка по дублированию тоже возможно с использовани- ем виртуальных методов, однако это потребует изменения метода в производных классах. 4.3.1 Виртуальные методы Виртуальные методы используют технологию позднего связывания, в которой определение того, метод какого класса должен быть вызван, производится во время работы программы не по типу переменной, а по объекту, ссылку на который она хранит. Для того, чтобы осуществлялся правильный расчёт площади фигуры, сделаем метод Area виртуальным, путём использования модификатора virtual в базовом классе Figure и модификатора override в производных классах Square и Rectangle . Также требуется убрать ключевое слово new в методе Area классов Square и Rectangle class Figure { 77 public virtual double Area() { return 0; } } class Square : Figure { public override double Area() { return _size*_size; } } class Rectangle : Figure { public override double Area() { return _size1*_size2; } } Figure f = new Figure(); string s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Не задан. Площадь: 0,00" f = new Square(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Квадрат. Площадь: 1,00" f = new Rectangle(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Прямоугольник. Площадь: 4,00" При переопределении виртуального метода в производном классе его назва- ние, возвращаемый тип, список параметров должны быть идентичны значениям в базовом классе. Поэтому, разработка виртуальных методов требует тщательного продумывания их структуры. Именно из-за необходимости поддерживать идентичность при переопределе- нии виртуальных методов невозможно простое превращение метода Duplicate в виртуальный, т.к. в разных классах данный метод возвращает объекты разных клас- сов. Решением проблемы могло бы стать изменение во всех классах возвращаемого типа на Figure , однако в этом случае при создании новых объектов путём дублиро- вания пришлось бы постоянно выполнять преобразование типов. В дальнейшем, из классов будет удалён метод Duplicate , т.к. наличия кон- структора копии вполне достаточно для дублирования объекта. Виртуальные методы не могут быть static , т.к. они подразумевают пере- определение в классах-потомках, что недопустимо для метода класса. 78 4.3.2 Абстрактные классы и члены классов Рассматривая разработанную выше иерархию классов можно отметить, что класс Figure фактически служит только для того, чтобы на основе его создать по- рождённые классы. Создание объекта такого класса не имеет смысла. Кроме того, в методе Area данного класса потребовалось возвращать какое-нибудь значение (воз- вращается 0), хотя рассчитывать площадь неопределённой фигуры бессмысленно. Для того, чтобы не описывать в базовом классе реализацию методов, которые вводятся в класс для обеспечения единообразия списка методов у порождённых классов, данные методы в базовом классе делают абстрактными. Абстрактный ме- тод имеет следующий особенности: перед методом указывается модификатор abstact ; метод является виртуальным, но модификатор virtual в базовом классе не ука- зывается; абстрактный метод не может быть статическим (т.е. у него не может быть моди- фикатора static ); в порождённом классе требуется указание модификатора override ; реализация абстрактного метода в базовом классе не требуется (да и не допу- стима); реализация абстрактного метода в порождённом классе обязательна, если дан- ный класс не является абстрактным. Если в классе имеется хотя бы один абстрактный метод, то весь класс стано- вится абстрактным и в строке заголовка класса должен быть указан модификатор abstract . Однако класс может быть объявлен абстрактным, даже если он не имеет ни одного абстрактного метода. Объект абстрактного класса не может быть создан. Абстрактными также могут быть свойства и индексаторы. Пример: модифицируем иерархию классов, сделав класс Figure и метод Area этого класса абстрактными. abstract class Figure { public abstract double Area(); } class Square : Figure { } class Rectangle : Figure { } Figure f; string s; 79 // f = new Figure(); Теперь данная строка недопустима f = new Square(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Квадрат. Площадь: 1,00" f = new Rectangle(); s = String.Format("{0}. Площадь: {1:N2}", f.sType, f.Area()); // s = "Прямоугольник. Площадь: 4,00" 4.3.3 Операторы as и is В предыдущем примере использовались возможности полиморфизма, позво- ляющие присвоить переменной класса Figure объекты порождённых классов: Figure f = new Square(); Хотя мы и знаем, что в переменной f хранится ссылка на объект класса Square , обратиться к свойству size этого объекта без дополнительных преобразо- ваний будет невозможно, т.к. базовый класс не имеет такого свойства. Одним из возможных способов преобразования является приведение типов, которое может быть выполнено следующим образом: ((Square)f).size = 5; 1 Однако явное преобразование при работе со ссылочными типами использо- вать не рекомендуется. Целесообразнее использовать специальный оператор преоб- разований ссылочных типов as , формальная схема которого имеет вид: <объект> as <требуемый тип> При помощи данного оператора присвоение вышеприведённый пример рабо- ты со свойством size может быть записан следующим образом: (f as Square).size = 5; Если преобразование выполнить невозможно, то возвращается null , напри- мер: public class Class1 {} public class Class2 : Class1 {} public class Class3 : Class1 {} Class2 cl2 = new Class2(); Class3 cl3 = new Class3(); Class1 cl1; Class2 cl; cl1 = cl2; cl = cl1 as Class2; 1 Строка (Square)f.size = 5; будет неправильной, т.к. операция обращения к члену класса выполняется ранее операции приведения типа 80 if (cl != null) // Истина textBox1.Text = cl.ToString();// Выполняется эта строка else textBox1.Text = "null"; cl1 = cl3; cl = cl1 as Class2; if (cl != null) // Ложь textBox2.Text = cl.ToString(); else textBox2.Text = "null"; // Выполняется эта строка Ещё одним оператором, используемым при работе с ссылочными типами, яв- ляется оператор is , используемый для проверки принадлежности объекта к задан- ному типу 1 . Формальная схема данного оператора имеет вид: <объект> is <проверяемый тип> Оператор возвращает true , если объект может быть преобразован к проверя- емому типу и false в противном случае (в том числе, если переменная имеет зна- чение null ). При проверке рассматривается внутреннее устройство объекта, а не тип переменной, которая проверяется. Пример: имеется массив, каждый элемент которого может хранить ссылку на объект класса, порождённого от класса Figure . Требуется присвоить свойству size каждого объекта класса Square значение 3, а свойствам size1 и size2 каждого объекта класса Rectangle значения 4 и 5 соответственно. Figure[] mas = new Figure[???]; for (int i=0; i (mas[i] as Square).size = 3; else if (mas[i] is Rectangle) { (mas[i] as Rectangle).size1 = 4; (mas[i] as Rectangle).size2 = 5; } } Как видно из примера, использование возможностей полиморфизма, а также операторов as и is позволяет производить обработку объектов разных (но имею- щих единого родителя) классов единообразно. 1 Правильнее сказать, что оператор is проверяет возможность преобразования. Для точной проверки сравни- вается результат запроса типа объекта с помощью метода Object.GetType() с результатом получения типа с по- мощью оператора typeof(<тип>). Если типы совпадают, то оба результата должны ссылаться на один и тот же объект типа System.Type. 81 4.3.4 Модификатор sealed Несмотря на эффективность и полезность наследования, оно иногда бывает не желательным. Если на основе некоторого класса не может быть создано порождён- ных классов, то при описании класса используется модификатор sealed , например: class MyClass1 { public int a; } sealed class MyClass2 : MyClass1 // Такое наследование допустимо { public double b; } class MyClass3 : MyClass2 // А это уже не допустимо { public int c; } Модификатор sealed не может быть применён для абстрактного класса, т.к. абстрактный класс подразумевает наследование от него. Модификатор sealed может также применяться при описании перегружен- ных виртуальных методов (т.е. описанных с модификатором override ) для предот- вращения дальнейшей их перегрузки, например: class MyClass1 { public virtual void Method1() { ... } public virtual void Method2() { ... } } class MyClass2 : MyClass1 { public sealed override void Method1() { ... } public override void Method2() { ... } } class MyClass3 : MyClass2 { public override void Method1() { ... } // Недопустимо public override void Method2() { ... } } 4.4 Перегрузка операторов В языке C# перегруженными могут быть не только методы, но и операторы. Перегрузка операторов позволяет применять обычные операторы для операций над 82 созданными классами. Перегруженными могут быть как унарные, так и бинарные операторы. Формальная запись перегрузки оператора имеет вид: для унарных: public static <возвращаемый тип> operator <оператор> (<тип операнда> <операнд>) {<операции>} для бинарных: public static <возвращаемый тип> operator <оператор> (<тип операнда1> <операнд1>,<тип операнда2> <операнд2>) { <операции> } где: <возвращаемый тип> – тип результата выполненной операции. Чаше всего сов- падает с типом класса, для которого перегружается оператор; <оператор> – перегружаемый оператор; <тип операнда> – тип одного из операндов. Для унарного оператора должен совпадать с типом класса. Для бинарного оператора должен совпадать с типом класса хотя бы у одного операнда; <операнд> – идентификатор одного из операндов; <операции> – действия, выполняемые для получения требуемого результата. При перегрузке операторов следует учитывать следующие особенности: не все операторы могут быть перегружены в том или ином классе (зависит от назначения и логики класса); всегда в методе, описывающем перегрузку оператора, создаётся новый объект (а не модифицируется существующий); в большинстве случаев требуется наличие в классе конструктора по умолчанию (без параметров); модификаторы ref и out не допустимы при описании параметров перегружаемо- го оператора. Пример: жидкость характеризуется объёмом и плотностью. Создать класс, описывающий жидкость, и обеспечить перегрузку некоторых операций сложения, вычитания, сравнения. public class Liquid { double Ro, V; public Liquid(double NewRo, double NewV) { Ro = NewRo; V = NewV; } public Liquid() : this(0, 0) { } // Сложение двух жидкостей public static Liquid operator +(Liquid L1, Liquid L2) { Liquid result = new Liquid(); double M = L1.Ro * L1.V + L2.Ro * L2.V; // Расчет массы result.V = L1.V + L2.V; // Расчет объёма 83 result.Ro = M / result.V; // Расчет плотности return result; } // Сложение жидкости и числа (увеличение объема) public static Liquid operator +(Liquid L, double V) { Liquid result = new Liquid(); result.Ro = L.Ro; result.V = L.V + V; return result; } // Сложение числа и жидкости (увеличение объема) public static Liquid operator +(double V, Liquid L) { return L + V; } // Вычитание числа из жидкости (уменьшение объема) public static Liquid operator -(Liquid L, double V) { Liquid result = new Liquid(); result.Ro = L.Ro; // Контроль за неотрицательностью объема result.V = L.V > V ? L.V - V : 0; return result; } // Увеличение объема на 1 public static Liquid operator ++(Liquid L) { return L + 1; } // Уменьшение объема на 1 public static Liquid operator --(Liquid L) { return L - 1; } // Проверка на равенство public static bool operator ==(Liquid L1, Liquid L2) { return (L1.Ro == L2.Ro) && (L1.V == L2.V); } // Проверка на неравенство public static bool operator !=(Liquid L1, Liquid L2) { return !(L1 == L2); } // Перегрузка методы выдачи строкового представления класса public override string ToString() { return String.Format("Ro: {0:F2}; V: {1:F2}", Ro, V); } } 84 Liquid L1 = new Liquid(100, 2); Liquid L2 = new Liquid(200, 3); Liquid L3; bool b; // Ниже приведены значения L3 и b L3 = L1 + L2; // Ro: 160,00; V: 5,00 L3 = L1 + 2; // Ro: 100,00; V: 4,00 L3 = 3 + L1; // Ro: 100,00; V: 5,00 L3 = L1 - 1.5; // Ro: 100,00; V: 0,50 L3 = L1 - 2.5; // Ro: 100,00; V: 0,00 L3++; // Ro: 100,00; V: 1,00 L3 = ++L3 - 0.5; // Ro: 100,00; V: 1,50 --L3; // Ro: 100,00; V: 0,50 L3--; // Ro: 100,00; V: 0,00 L3 = L1; b = L1 == L3; // b = True b = L1 != L3; // b = False В приведённом выше примере также можно было бы определить оператор сравнения жидкости с дробным числом (например, через объем). Операторы == и != , < и > , <= и >= |