Конспект лекций (C#)-unlocked. 1 Основные сведения о C# Особенности языка
![]()
|
Пример: класс, описывающий квадрат, с расчётным свойством, работающим по принципу «только для чтения». class Square { private double _side; public double side // сторона { get { return _side; } set { if (value > 0) _side = value; } } public double area // площадь { get { return Math.Pow(_side, 2); } } } Методы get и set по умолчанию имеют такой же уровень доступа, как и само свойство (т.е. в большинстве случаев public ). Однако им могут быть указаны соб- ственные спецификаторы доступа. Например, если требуется, чтобы значение свой- ства могло быть изменено только из самого класса, то свойство может быть описано следующим образом: class MyClass { public int p { get {...} private set{...} } } В классе могут быть объявлены автоматически реализуемые свойства. Та- кие свойства не имеют реализации методов get и set и не требуют явного указания поля для хранения значения, т.к. такое поле выделяется для свойства автоматически. Пример автоматически реализуемого свойства: 62 class MyClass { public int p { get; set; } } На значения автоматически реализуемых свойств нельзя наложить никаких ограничений, их нельзя сделать «только для чтения» или «только для записи» 1 . По- этому применение таких свойств достаточно ограничено. 3.10 Индексаторы Индексаторы позволяют рассматривать объект класса как массив, т.е. обра- щаться к его элементам с использованием индексов, указываемых в квадратных скобках. Формальная структура описания индексатора имеет вид: [<доступ>] <тип> this[<список индексов>] {get {<код метода доступа get>} set {<код метода доступа set>}} где: <список индексов> – набор индексов, перечисленных через запятую, каждый из которых имеет структуру: <тип> <идентификатор>; чаще всего <тип> является целым, хотя может быть любым; <код метода доступа get> – код, определяющий действия, выполняемые при запросе значения индексатора и использующий индексы, определённые в <список индексов> ; <код метода доступа set> – код, определяющий действия, выполняемые при установке значения индексатора и использующий индексы, определённые в <список индексов> Метод доступа get должен содержать оператор return , определяющий зна- чение, возвращаемое методом get при запросе значения индексатора. Метод доступа set имеет неявно заданный параметр value , который позво- ляет указать значение, присваиваемое индексатору. Как и свойство, индексатор обычно связан с полем, имеющим спецификатор доступа private или protected . В большинстве случаев, данное поле является массивом типа <тип> , имеющим количество размерностей, равное количеству па- раметров в <список индексов> Пример: класс, имеющий одномерный индексатор, возвращающий ние -1, если происходит обращение к элементу с недопустимым индексом. 1 Подразумевается, что нельзя пропустить метод get или set. Однако это можно реализовать используя спе- цификаторы доступа для этих методов 63 class MyClass { private int[] _mas; public MyClass(int count) { if (count > 0) _mas = new int[count]; else _mas = new int[0]; } public int this[int index] { get { return (index >= 0 && index < _mas.Length) ? _mas[index] : -1; } set { if (index >= 0 && index < _mas.Length) _mas[index] = value; } } } MyClass cl = new MyClass(10); cl[5] = 7; int i1 = cl[4]; // i1 = 0; int i2 = cl[5]; // i2 = 7; int i3 = cl[-1]; // i3 = -1; int i4 = cl[10]; // i4 = -1; Пример: класс описывающий шахматную доску и имеющий двухмерный ин- дексатор с индексами типа char (буквы от 'a' до 'h') и типа byte (цифры от 1 до 8) воз- вращающий тип фигуры, стоящей на заданной клетке, признак пустой клетки или значение ошибки, если происходит обращение к элементу с недопустимым индек- сом. enum ChessFigure {King,Queen,Rook,Bishop,Knight,Pawn,Empty,Error}; class MyClass { private string s = "abcdefgh"; private ChessFigure[,] _mas; public MyClass() { _mas = new ChessFigure[8,8]; for (int i=0; i<8; i++) for (int j=0; j<8; j++) _mas[i,j] = ChessFigure.Empty; } public ChessFigure this[char c,byte n] { get 64 { int i = s.IndexOf(c); if (i >= 0 && n >= 1 && n <= 8) return _mas[i,n-1]; else return ChessFigure.Error; } set { int i = s.IndexOf(c); if (i >= 0 && n >= 1 && n <= 8) _mas[i,n-1] = value; } } } MyClass cl = new MyClass(); cl['b',3] = ChessFigure.Queen; ChessFigure f1 = cl['b',3]; // f1 = Queen ChessFigure f2 = cl['e',8]; // f2 = Empty ChessFigure f3 = cl['k',2]; // f3 = Error ChessFigure f4 = cl['h',0]; // f4 = Error Как и у свойства, у индексатора могут отсутствовать методы get или set для создания индексаторов «только для записи» или «только для чтения» соответствен- но. Индексатор может быть перегружен. В этом случае при обращении использу- ется тот из индексаторов, у которого индексы наиболее подходят заданным. Поэто- му при перегрузке индексаторов количество и/или тип индексов должны отличаться. Пример: класс, позволяющий получить либо доступ к заданному элементу матрицы, либо сумму элементов заданной строки матрицы с помощью индексато- ров. Считать, что значение элемента или суммы элементов строки при неправиль- ном задании индекса равно -1. class MyClass { private int[,] _mas; public MyClass(int countRow,int countCol) { if (countRow > 0 && countCol > 0) _mas = new int[countRow,countCol]; else _mas = new int[0,0]; } public int this[int indexRow,int indexCol] { get { if (indexRow >= 0 && indexRow < _mas.GetLength(0) && indexCol >= 0 && indexCol < _mas.GetLength(1)) return _mas[indexRow,indexCol]; else 65 return -1; } set { if (indexRow >= 0 && indexRow < _mas.GetLength(0) && indexCol >= 0 && indexCol < _mas.GetLength(1)) _mas[indexRow,indexCol] = value; } } public int this[int indexRow] { get { if (indexRow > 0 && indexRow < _mas.GetLength(0)) { int sum=0; for (int i=0; i < _mas.GetLength(1); i++) sum += _mas[indexRow,i]; return sum; } else return -1; } } } MyClass cl = new MyClass(3,3); for (int i=0; i<3; i++) for (int j=0; j<3; j++) cl[i,j] = i*3+j+1; int i1 = cl[1,0]; // i1 = 4 int i2 = cl[3,3]; // i2 = -1 int i3 = cl[1]; // i3 = 15 int i4 = cl[4]; // i4 = -1 При использовании индексаторов имеются следующие ограничения: индексатор нельзя передавать в метод в качестве параметра ref или out ; индексатор не может быть static (назначение данного ключевого слова будет рассмотрено ниже). 66 4 Классы. Расширенное использование 4.1 Статические классы и члены классов Применение ключевого слова static при объявлении члена класса указывает, что данный член класса принадлежит всему классу, а не конкретному объекту. Такой член класса существует все время выполнения программы, даже если ни одного объекта этого класса не создано. Доступ к такому члену класса возможен только через сам класс, а не через объекты класса. Пример: создание класса, подсчитывающего количество объектов этого клас- са, созданных за время выполнения программы. class MyClass { public static int count; public MyClass() { count++; } } int i1 = MyClass.count; // i1 = 0; MyClass c; for (int i=0; i<5; i++) c = new MyClass(); int i2 = MyClass.count; // i2 = 5; Фактически статический член-данные является глобальной переменной в рам- ках некоторого класса. Поэтому лучше работу с такими членами-данными класса осуществлять через статические методы класса (обратиться через обычные методы тоже возможно, но ведь для этого необходимо создать объект!) . Например, преды- дущий пример можно записать следующим образом: class MyClass { private static int count; public MyClass() { count++; } public static int GetCount() { return count; } } int i1 = MyClass.GetCount(); // i1 = 0; MyClass c; for (int i=0; i<5; i++) 67 c = new MyClass(); int i2 = MyClass.GetCount(); // i2 = 5; Так как статические члены-методы вызываются через класс, то при описании их реализации есть некоторые ограничения: в теле метода должно отсутствовать ключевое слово this , т.к. метод выполняется безотносительно к какому-либо объекту; в теле метода допускается вызов только статических членов-методов класса (но возможен вызов нестатических методов через объект, переданный в метод в каче- стве параметра); в теле метода возможно обращение только к статическим членам-данным класса. Статическим может быть и конструктор, который применяются для инициали- зации статических членов-данных. Однако при описании такого конструктора не указывается спецификатор доступа (т.е. он имеет доступ по умолчанию – private – и не может быть явно вызван). Например, класс, описанный в предыду- щем примере, может быть расширен статическим конструктором (хотя с точки зре- ния программы он и излишен) следующим образом: class MyClass { private static int count; static MyClass() { count = 0; } public MyClass() { count++; } public static int GetCount() { return count; } } Статический конструктор не может иметь параметров, и, следовательно, не может быть перегружен. Вызов статического конструктора выполняется автоматически при запуске программы, использующей класс. Статическими могут быть не только отдельные члены класса, но и класс цели- ком. Это достигается применением ключевого слова static в описании самого класса. Статический класс обладает двумя основными свойствами: объекты статического класса создать нельзя; статический класс должен содержать только статические члены. Одно из основное назначение статического класса – создание класса, объеди- няющего статические члены, назначение или физическая сущность которых схожи. Примером статического класса может служить класс Math 68 4.2 Наследование Наследование является одним из трёх основополагающих принципов объект- но-ориентированного программирования, поскольку оно допускает создание иерар- хических классификаций. Класс, который наследуется, называется базовым, а класс, который наследует, – производным. Следовательно, производный класс представляет собой специали- зированный вариант базового класса. Он наследует все переменные, методы, свой- ства и индексаторы (НО НЕ КОНСТРУКТОРЫ), определяемые в базовом классе, добавляя к ним свои собственные элементы. Если вновь создаваемый класс является производным от некоторого базового класса 1 , то после идентификатора производного класса ставится двоеточие, после которого указывается идентификатор базового класса. Пример: Создание класса «квадрат» на основе разработанного выше класса «фигура», дополнение его свойством «размер» (с допустимым значением >0) и ме- тодом расчёта площади. class Square : Figure { private double _size; public double size { get { return _size; } set { if (value > 0) _size = value; } } public double Area() { return _size*_size; } } Square square = new Square(); square.sType = "Квадрат"; square.posX = square.posY = 1; square.size = 5.1; double d = square.Area(); // d = 26.01 Figure figure = square.Duplicate(); При подробном рассмотрении приведённого выше кода (в том числе и анализе кода класса Figure ) можно выделить несколько негативных моментов, требующих доработки как производного, так и базового классов: 1. т.к. наследование конструкторов не осуществляется, то при создании объекта используется конструктор по умолчанию, который присваивает «нулевое» зна- чение полю size . Это нарушает правила работы класса, т.к. если не будут явно указаны все значения полей, то объект будет содержать поля с недопустимыми 1 В отличии от C++, в С# базовый класс может быть только один, т.е. не допускается множественное наследо- вание. 69 значениями. Поэтому для класса Square требуется описание, как минимум, кон- структора без параметров; 2. несмотря на то, что у класса Square значение поля _sType может быть только «Квадрат» (т.к. класс описывает именно такую фигуру), при использовании класса возможно присвоение этому полю произвольного значения. Для устране- ния этого недочёта в базовом классе свойство sType должно быть «только для чтения»; 3. метод Duplicate создаст объект класса Figure , что будет неверным с точки зрения назначения этого метода. Поэтому метод должен быть переопределён; 4. т.к. в класса Figure поля _sType , _posX и _posY объявлены со спецификато- ром доступа private , то доступ к ним в производном класса возможен только через свойства, хотя поля и являются часть производного класса. Для устранения этого недостатка 1 с сохранением блокировки доступа из-за пределов производ- ных классов, нужно сменить спецификатор доступа у этих полей в базовом клас- се на protected Изменённый в соответствии с пунктами 2 и 4 класс Figure будет выглядеть следующим образом: class Figure { protected string _sType; protected int _posX, _posY; public string sType { get { return _sType; } } 2 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) 1 В принципе, возможность доступа через свойства не является недостатком, т.к. обеспечивает защищенность данных, хотя и уменьшает быстродействие программы. В данном примере, изменение спецификатора доступа на protected используется для его демонстрации. Применение этого спецификатора полностью оправдано, когда име- ется поле, которое не связано с каким-либо свойством для блокировки доступа к нему извне, но должно быть доступ- но в производных классах. 2 Жирностью выделен измененный или добавленный код. 70 { _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); } } Далее рассматриваются способы устранения недостатков пунктов 1 и 3. 4.2.1 Наследование и конструкторы Для устранения недостатка 1 дополним класс Square конструктором без па- раметров, и конструктором копии: class Square : Figure { private double _size; public double size { get { return _size; } set { if (value > 0) _size = value; } } public double Area() { return _size*_size; } public Square() { _sType = "Квадрат"; _size = 1; } public Square(Square sourceSquare) { _sType = sourceSquare._sType; _posX = sourceSquare._posX; _posY = sourceSquare._posY; _size = sourceSquare._size; } } 71 Создание в производном классе конструкторов возможно, если выполняется одно из трёх условий: базовый класс не имеет конструкторов. В этом случае выполняется конструк- тор по умолчанию для базового класса, а потом конструктор производного класса; базовый класс имеет конструкторы, и среди них есть конструктор без пара- метров. В этом случае сначала выполняется конструктор без параметров базово- го класса, а потом конструктор производного класса; производный класс явно вызывает конструктор базового класса. В этом случае сначала вызывается указанный конструктор базового класса, а потом конструктор производного класса. Если базовый класс в свою очередь является производным от другого класса, то вызов его конструктора будет выполняться по этой же схеме, т.е. при многоуров- невой иерархии всегда сначала вызываются конструкторы классов более высокого уровня, а уже потом более низкого. Для создания конструктора, явно вызывающего конструктор базового класса, используется конструкция вида: <доступ> <имя_класса>([<список параметров1>]) : base([<список параметров2>]) { <тело конструктора> } Здесь <список параметров1> определяет набор формальных параметров конструктора производного класса, а <список параметров2> – набор фактиче- ских параметров вызываемого конструктора базового класса. При этом, в <список параметров2> могут использоваться параметры из <список параметров1> Пример: изменение конструктора без параметров у класса Square public Square() : base("Квадрат") { _size = 1; } 4.2.2 Переопределение членов класса Для устранения недостатка 3 добавим в класс Square переопределённый ва- риант метода Duplicate : class Square : Figure { private double _size; public double size { get { return _size; } set { if (value > 0) _size = value; } } 72 public double Area() { return _size*_size; } public Square() : base("Квадрат") { _size = 1; } public Square(Square sourceSquare) { _sType = sourceSquare._sType; _posX = sourceSquare._posX; _posY = sourceSquare._posY; _size = sourceSquare._size; } public Square Duplicate() { return new Square(this); } } При переопределении члена-метода с тем же набором параметров, что и у ба- зового класса, возникает эффект сокрытия метода базового класса, о чем компиля- тор сообщит в виде предупреждения. Если такое переопределение делается созна- тельно, то для устранения предупреждения перед возвращаемым типом указывается ключевое слово new . Например, предыдущий вариант описания заголовка метода Duplicate может быть записан так: new public Square Duplicate() Если в производном классе при переопределении метода изменяется число па- раметров, то сокрытия |