struct MyVal
{
Ref r;
Val v;
public override bool Equals(Object obj)
{
if (!(obj is MyVal))
return false;
return Equals((MyVal)obj);
}
public bool Equals (MyVal obj)
{
if (!Object. Equals(r, obj. r))
return false;
if (!v. Equals(obj. v))
return false;
return true;
}
public static bool operator==(MyVal v1, MyVal v2)
{
return (v1.Equals(v2));
}
public static bool operator!=(MyVal v1, MyVal v2)
{
return!(v1==v2);
}
}
Помимо Equals, в классе Object определен также статический метод ReferenceEquals, который позволяет узнать, являются ли две ссылки ссылками на один и тот же объект:
class Object
{
public static bool ReferenceEquals
(Object objA, Object objB)
{
return (objA == objB);
}
}
Модификаторы доступа и предопределенные атрибуты
Ниже приводятся модификаторы доступа для типов, полей или методов:
private | доступен только методам в определяющем типе и вложенных в него типах |
protected | доступен только методам в этом типе (и вложенных в него типах) или одном из его производных типов безотносительно к сборке. |
internal | доступен только методам в определяющей сборке |
protected internal | доступен только методам в этом типе, любых производных типах или любых типах, заданных в определяющей сборке |
public | доступен всем методам во всех сборках |
Кроме того, для типов можно пользоваться атрибутами:
- abstract – не может быть создан экземпляр типа. Тип может использоваться как базовый тип для другого типа. Если производный тип не является абстрактным, могут быть созданы его экземпляры. sealed – тип не может использоваться в качестве базового
CLR позволяет отмечать типы как abstract или sealed, но не оба сразу. Однако можно пометить тип как sealed и задать закрытый конструктор без параметров, который никогда не вызывается. Задание закрытого конструктора не дает компилятору автоматически создавать открытый конструктор без параметров. Поскольку код вне этого типа не может обратиться к конструктору, не могут быть созданы экземпляры этого типа.
Предопределенные атрибуты поля:
- static – поле частично определяет состояние типа, а не объекта. readonly – в поле может осуществляться запись только кодом, находящимся в конструкторе.
CLR позволяет отмечать поля как static и readonly одновременно.
Предопределенные атрибуты метода:
- static – метод связан напрямую с типом, а не его экземпляром. Статические методы не могут обращаться к экземплярным полям или методам, определенным в данном типе, так как статический метод не знает ни о каких объектах virtual – вызывается ближайший метод-предок, даже если объект приводится к базовому типу. Применяется только к экземплярным (нестатическим) методам. new – метод не должен переопределять экземплярный метод, определенный в базовом типе; метод скрывает унаследованный метод. Применяется только к экземплярным методам. override – явно указывает, что метод переопределяет виртуальный в исходном типе. Применяется только к производным методам. abstract – указывает, что производный тип должен реализовать метод с сигнатурой, соответствующей этому абстрактному методу. Тип с абстрактным методом является абстрактным. Применяется только к виртуальным методам. sealed – производный тип не может переопределять этот метод. Применяется только к производным методам.
Любой полиморфный экземплярный метод может быть помечен как abstract или sealed, но не сразу обоими атрибутами.
Перегрузка операций
В C# реализована перегрузка операций для типов, разработанных пользователем. Нельзя перегружать операции =, [ ] (для этого случая следует использовать индексные свойства). Операции перегружаются как публичные статические функции данного типа (для бинарных операций один из параметров обязательно должен иметь данный тип)
Приведем пример перегрузки операции + для класса Complex комплексных чисел:
class Complex
{
private double r, i;
public static Complex operator+
(Complex c1, Complex c2)
{
return new Complex(c1.r+c2.r, c1.i+c2.i);
}
}
Аналогично перегружаются и остальные операции.
Методы операторов преобразования
Чтобы выполнить преобразование объекта, тип должен определять специальные операции преобразования. если требуется, чтобы преобразование было неявным, у операции требуется указывать модификатор explicit, если же преобразование может быть только явным, то следует использовать модификатор implicit. После ключевого слова operator надо указать целевой тип, в который преобразуется объект, а в скобках – исходный тип объекта. Например, для явного преобразования объектов класса Frac дробей к типу double и для неявного преобразования целого к Frac используется следующий код:
class Frac
{
...
public Frac (int m, int n) {...}
public double ToDouble( ) {...}
public static explicit operator double (Frac j)
{
return j. ToDouble();
}
public implicit operator Frac (int i)
{
return new Frac(i,1);
}
}
Ключевое слово implicit указывает компилятору, что наличие в исходном тексте явного приведения типов не обязательно для генерации кода, вызывающего метод оператора преобразования. Ключевое слово explicit позволяет компилятору вызывать метод лишь когда в исходном тексте имеется явное приведение типов.
Свойства
Свойства классов представляют собой аналоги полей, при доступе к которым на чтение и запись выполняются некоторые действия. Рассмотрим пример:
public class Student
{
private int age;
public int Age
{
get
{
return age;
}
set
{
if (value<0)
throw new BadAge(age);
age = value;
}
}
}
Здесь при доступе к свойству age на чтение просто возвращается его значение, при доступе же на запись проверяется, не присваивается ли age значение меньше нуля, и если да, то генерируется исключение BadAge. Следует обратить внимание, что в методе доступа на запись неявно определена переменная value с типом, совпадающим с типом свойства так, как если бы был определен метод
setAge(int value)
При определении свойства компилятор генерирует и помещает в результирующий управляемый модуль:
- метод-аксессор get этого свойства – генерируется, только если для свойства определен аксессор get; метод-аксессор set этого свойства – генерируется, только если для свойства определен аксессор set; определение свойства в метаданных управляемого модуля – генерируется всегда.
Наследование
Класс можно унаследовать от другого класса, при этом все поля, свойства и методы базового класса наследуются производным. Кроме того, производный класс может определять дополнительные поля, методы и свойства, а также переопределять уже существующие.
Наследование выполняет две основные функции – наследование реализации, предотвращающее дублирование кода, и наследование интерфейса, позволяющее обращаться к объектам разных классов через один и тот же интерфейс.
Рассмотрим пример наследования класса Student от класса Person
class Person
{
private string name;
private int age;
public Person(string name, int age)
{
this. name = name;
this. age = age;
}
}
class Student: Person
{
private int group;
public Student(string name, int age, int group):
base(name, age)
{
this. group = group;
}
}
Здесь следует обратить внимание, что вызов конструктора предка осуществляется в списке инициализации потомка (как и в C++), при этом вместо имени базового класса используется слово base.
Полиморфизм и виртуальные функции
При замещении функции в подклассе ее можно вызывать через объект базового класса. Для этого функцию необходимо объявить как виртуальную, используя модификатор virtual, а при замещении ее в подклассах необходимо использовать модификатор override. Рассмотрим пример:
class Person
{
...
public virtual void Print()
{
Console. Write(”{0} {1}”,name, age);
}
}
class Student: Person
{
...
public override void Print()
{
base.Print();
Console.Write (” {0}”,group);
}
}
Опишем переменную базового класса и инициализируем ее объектом производного класса:
Person p = new Student(“Иванов”,19,4);
После этого вызовем метод Print через переменную p:
p. Print();
Какой метод Print вызовется – класса Person или класса Student? Если бы отсутствовали слова virtual и override, то вызвался бы метод Print базового класса Person. В данном коде же вызовется метод Print класса Student. Поскольку решение о том, какой метод вызывать, принимается на этапе выполнения программы (т. е. «поздно»), то говорят, что имеет место позднее связывание имени функции с конкретным телом. Способность пременных классов в зависимости от истинного (динамического) типа, в них содержащегося, вызывать разные (сходные по смыслу) методы называется полиморфизмом. Таким образом, полиморфизм реализуется через механизм виртуальных функций.
Рассмотрим другой пример: в классе Object определен виртуальный метод
public virtual string ToString();
преобразующий значение объекта в строку. Для любого класса этот метод желательно перегружать. Сделаем это в нашем случае:
class Person
{
...
public override string ToString()
{
return string. Format(”{0} {1}”,name, age);
}
}
class Student: Person
{
...
public override string ToString()
{
return string. Format(”{0} {1}”,base. ToString(),
group);
}
}
Проектное задание
Создать иерархию классов Person, Student, PostGraduate, Teacher, Chair, Course. Каждый преподаватель должен работать на какой-то кафедре и вести 1 и более курсов. Каждый студент может иметь не более одного научного руководителя. Каждый преподаватель может руководить несколькими студентами. Каждый аспирант должен быть прикреплен к какой-то кафедре и обязательно иметь научного руководителя. В каждом классе требуется переопределить метод Equals, а также операции == и!=.
Тест рубежного контроля
1. Какой модификатор по умолчанию используется для классов?
a. public
b. private
c. internal
2. Какой модификатор по умолчанию используется для членов?
a. public
b. private
c. internal
d. protected
3. При наследовании конструктор базового класса вызывается
a. До конструктора производного класса
b. Может вообще не вызываться, если мы его явно не вызовем
c. Вызывается в тот момент, когда мы явно укажем его вызов в коде конструктора подкласса
Модуль 4. Интерфейсы. Коллекции
Комплексная цель
Освоить использование стандартных интерфейсов, научиться определять собственные интерфейсы, пользоваться множественным наследованием интерфейсов. Рассмотреть основные классы коллекций и типичные задачи с их использованем.
Содержание
Интерфейсы
Интерфейс – это легковесный класс, где все функции являются виртуальными и отсутствуют поля (однако, могут присутствовать свойства). Количество функций, определенных в конкретном интерфейсе, зависит от того, какое поведение мы пытаемся смоделировать при помощи этого интерфейса. С точки зрения синтаксиса интерфейсы в C# определяются следующим образом:
public interface IPoint
{
byte GetNumberOfPoints();
}
В данном примере функция GetNumberOfPoints автоматически становится виртуальной.
Интерфейсы. NET также могут поддерживать любое количество свойств (и событий). Например, интерфейс IPoints содержит свойство Points для чтения и записи:
public interface IPoint
{
int Points (get; set;)
}
В любом случае интерфейс – это не более чем именованный набор абстрактных членов, а это значит, что любой класс, реализующий этот интерфейс, должен самостоятельно полностью определять каждый из членов этого интерфейса. Более того, каждый определяемый метод автоматически становится виртуальным – слово override использовать не надо. Таким образом, интерфейсы – это еще один способ реализации полиморфизма в приложении: поскольку в разных классах функции одних и тех же интерфейсов будут реализованы по-разному, в результате эти классы будут реагировать на одни и те же вызовы по-своему.
Интерфейс – это чистая синтаксическая конструкция, которая предназначена только для определенных целей. Интерфейсы никогда не являются типами данных, и в них не бывает реализаций методов по умолчанию. Каждый член интерфейса (будь то свойство или метод) автоматически становится виртуальным. В C# множественное наследование запрещено; в то же время реализация в классе нескольких интерфейсов (т. е. наследование нескольких интерфейсов) – это обычное дело.
Реализация интерфейса
Когда в C# какой-либо класс должен реализовывать нужные нам интерфейсы, названия этих интерфейсов должны быть помещены (через запятую) после двоеточия, следующего за именем класса. Имя базового класса (если оно есть) должно стоять перед именами любых интерфейсов:
public class Triangle: Shape, IPoint
{
public Triangle (){}
public Triangle (string name) : base(name){}
//Реализация IPoint
public byte GetNumberOfPoints()
{
return 3;
}
}
В классе должны быть реализованы все методы используемого интерфейса, в противном случае произойдет ошибка компиляции.
Наследование интерфейсов и наследование реализации
Наследование интерфейсов (наследование от интерфейса) позволяет определить всю реализацию с нуля, также в наследовании интерфейсов нет проблемы с одинаковыми именами функций. Хорошо все начинать с чистого листа, но это сложно и на практике часто необходимо использовать ранее разработанную реализацию, используя обычное наследование. Но в наследовании реализации есть некоторые проблемы. Часто при наследовании реализации нам необходима лишь часть функций из наследуемого класса, остальная реализация становится грузом, причем не только для нашего класса, но и для тех классов, которые будут унаследованы от нашего.
Рассмотрим ситуации, когда выбирается наследование интерфейсов, а когда наследование реализации.
Пример1. Наследование реализации.
Класс Control, от него унаследованы Button, CheckBox, ListBox. В классе Control реализована большая функциональность, а классы Button, CheckBox, ListBox наследуют всю эту функциональность и вносят некоторые небольшие дополнения. Таким образом, классы Button, CheckBox, ListBox сильно связаны и основной код содержится в единственном виде в реализации класс Control. В этом случае оправдано использование наследование реализации.
Пример 2. Наследование интерфейса.
Классы ArrayList, Queue, HashTable реализуют в себе несколько интерфейсов: IList, IEnumerable, ICollection. Внутри классы ArrayList, Queue, HashTable реализованы абсолютно по разному, т. е. слабо связаны, поэтому здесь оправдано использование наследование интерфейса.
Получение ссылки на интерфейс
Первый способ – воспользоваться явным приведением типов:
Triangle tr = new Triangle();
IPoint p = (IPoint) tr;
Console. WriteLine (p. GetNumberOfPoints());
Однако если мы попробуем применить то же самое приведение типов к объекту класса, не поддерживающего IPont, мы получим сообщение об ошибке времени выполнения. Когда мы пытаемся получить ссылку на интерфейс путем явного приведения типов для объекта класса, не поддерживающего данный интерфейс, система генерирует исключение InvalidCastExeption.
Чтобы избежать проблем с исключением нужно его перехватить:
Triangle tr = new Triangle ();
IPoint p;
try
{
p = (IPoint)tr;
Console. WriteLine (p. GetNumberOfPoints() );
}
catch (InvalidCastExeption e)
{
Console. WriteLine ("Not point");
}
Второй способ получить ссылку на интерфейс – использовать ключевое слово as:
Triangle tr = new Triangle();
IPoint p;
p = tr as IPoint;
if ( p!= null)
Console. WriteLine(p. GetNumberOfPoints());
else Console. WriteLine("Not point");
Если при использовании ключевого слова as мы попробуем создать ссылку на интерфейс через объект, который этот интерфейс не поддерживает, ссылка просто будет установлена в null, и при этом никаких исключений генерироваться не будет.
Третий способ получения ссылки на интерфейс – воспользоваться операцией is. Если объект не поддерживает интерфейс, условие станет равно false.
Triangle tr = new Triangle();
if (tr is IPoint)
Console. WriteLine(p. GetNumberOfPoints());
else Console. WriteLine("Not point");
Разрешение проблемы с одинаковыми именами функций
Опишем два интерфейса с функцией Menu. У интерфейсов она выполняет различные функции. В C# есть возможность реализовать оба интерфейса в одном классе, различая два Menu относящихся к разным интерфейсам.
interface IRestaurant
{
void Menu();
}
interface IWindow
{
void Menu();
}
class RestaurantForm: Form, IRestaurant, IWindow
{
void IRestaurnt. Menu(){...} //функция Menu, относящаяся к интерфейсу IRestaurant
void IWindow. Menu(){...} //функция Menu, относящаяся к интерфейсу IWindow
public void Menu(){...}//функция Menu самого RestaurantForm
}
Как этим воспользоваться?
void f (IReataurant r)
{
r. Menu();
}
RestaurantForm r = new RestaurantForm();
f(r); //вызовется функция Menu, относящаяся к интерфейсу IRestaurant
Если в программе написать просто
r. Menu(),
то вызовется Menu класса RestaurantForm, а не какого-либо интерфейса. Вызов
(r as IWindow).Menu();
приведет к вызову функции Menu интерфейса IWindow.
Если же в классе опредедела только последняя функция Menu, то во всех трех примерах вызовется именно она.
Изменение полей в упакованных размерных типах посредством интерфейсов
Если размерный тип упакован в ссылочный, то единственный способ изменить поля упакованного объекта – использование интерфейсов. Приведем соответствующий код:
interface IChange
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 |


