Главная страница

Конспект лекций (C#)-unlocked. 1 Основные сведения о C# Особенности языка


Скачать 1.97 Mb.
Название1 Основные сведения о C# Особенности языка
Дата15.01.2019
Размер1.97 Mb.
Формат файлаpdf
Имя файлаКонспект лекций (C#)-unlocked.pdf
ТипДокументы
#63730
страница8 из 13
1   ...   5   6   7   8   9   10   11   12   13
не происходит.
Аналогично, может осуществляться сокрытие членов-данных, например: 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{ if (mas[i] is Square)
(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
В приведённом выше примере также можно было бы определить оператор сравнения жидкости с дробным числом (например, через объем).
Операторы
==
и
!=
,
<
и
>
,
<=
и
>=
1   ...   5   6   7   8   9   10   11   12   13


написать администратору сайта