Конспект лекций (C#)-unlocked. 1 Основные сведения о C# Особенности языка
Скачать 1.97 Mb.
|
должны перегружаться попарно. При перегрузке операторов == и != также требуется переопределить методы Object.Equals и Object.GetHashCode 1 , например следующим образом: public override bool Equals(object obj) { if (obj != null && GetType() == obj.GetType()) return this == (obj as Liquid); else return false; } public override int GetHashCode() { return V.GetHashCode() ^ Ro.GetHashCode(); } Также могут быть перегружены и другие операторы. Однако ряд операторов перегрузить нельзя: все операторы присваивания, && , || , () , [] , , ? , ?? , => , -> , checked , unchecked , default , as , is , new , sizeof , typeof 1 Функция GetHashCode() должна для двух равных объектов возвращать одно и то же значение, но не обя- зана для двух различных объектов возвращать разные значения 85 5 Интерфейсы Как отмечалось выше, для абстрактного метода указывается только описание его заголовка, но не указывается реализация. Таким образом, абстрактный метод представляет собой интерфейс работы с методом. Абстрактный класс может сочетать в себе как абстрактные, так и обычные ме- тоды, свойства, индексаторы, а также хранить поля. Тем не менее, он может содер- жать и только абстрактные методы, свойства, индексаторы. Класс, содержащий только абстрактные методы, свойства, индексаторы, может быть реализован не только как абстрактный класс, но и как интерфейс. Формальная схема описания интерфейса имеет вид: interface <идентификатор интерфейса> [: <идентификатор интерфейса-родителя1>[, <идентификатор интерфейса-родителя2>...]] { <возвращаемый тип> <идентификатор метода1>([<список параметров>]); [<возвращаемый тип> <идентификатор метода2>([<список параметров>]); ...] <тип> <идентификатор свойства1> {get; set;} [<тип> <идентификатор свойства2> {get; set;} ...] <тип> this[<список параметров 1>] {get; set;} [<тип> this[<список параметров 2>] {get; set;} ...] } При описании членов интерфейса, доступ не указывается, т.к. по умолчанию доступ всех членов интерфейса public Реализация членов интерфейса должна быть выполнена в классе, реализую- щем интерфейс. При этом, при реализации членов интерфейса они должны быть объявлены со спецификатором доступа public 1 . Для указания того, что класс реа- лизует интерфейс используется конструкция: [<доступ>] class <идентификатор класса> : <идентификатор интерфейса> { <члены класса и реализация интерфейса> } Пример: создание и использование интерфейса с методом, свойством и ин- дексатором. Код реализации выбран произвольно. interface MyInterface { int Method(); // Объявление метода int Property // Объявление свойства 1 Спецификатор доступа public не указывается, если явно указывается имя интерфейса. Явное указание имени интерфейса необходимо для сокрытия членов интерфейса в классе или для реализации одноименных членов нескольких интерфейсов в одном классе. Примеры такой реализации будут рассмотрены ниже 86 { get; set; } int this[int n] // Объявление индексатора { get; set; } } class MyClass : MyInterface { private int x; private int[] mas = new int[10]; public int Method() // Реализация метода { return 1; } public int Property // Реализация свойства { get { return x; } set { x = value; } } public int this[int n] // Реализация индексатора { get {return (n >= 0 && n < mas.Length ? mas[n] : 0); } set {if (n >= 0 && n < mas.Length) mas[n] = value; } } } MyClass cl = new MyClass(); int i1 = cl.Method(); // i1 = 1 cl.Property = 5; int i2 = cl.Property; // i2 = 5; cl[1] = 4; int i3 = cl[0]; // i3 = 0; int i4 = cl[1]; // i4 = 4; Для сокрытия членов интерфейса при их реализации явно указывается имя ин- терфейса и не указывается спецификатор доступа, например: interface MyInterface { int A(); } class MyClass : MyInterface { int MyInterface.A() { return 1; } 87 public int ClassA() { return ((MyInterface)this).A(); } } MyClass cl = new MyClass(); int i; i = cl.A(); // Нет доступа к методу интерфейса i = cl.ClassA(); // i = 1; Класс может реализовывать несколько интерфейсов. В этом случае они пере- числяются после двоеточия через запятую. Если класс также является производным от некоторого базового класса, то после двоеточия сначала указывается базовый класс, а после него через запятую реализуемые интерфейсы. Таким образом, использование интерфейсов фактически обеспечивает мно- жественное наследование, пусть и в усечённой форме. Если класс реализует несколько интерфейсов, имеющих одинакового члена (совпадает его описание), то, по умолчанию, реализация в классе выполняется один раз и считается, что она относится к обоим интерфейсам, например: interface MyInterface1 { int Method(); } interface MyInterface2 { int Method(); } class MyClass : MyInterface1, MyInterface2 { public int Method() // Реализация метода обоих интерфейсов { return 1; } } MyClass cl = new MyClass(); int i = cl.Method(); // i = 1 Однако если требуется, чтобы реализация у интерфейсов была разной, необходимо описать каждую из них отдельно с указанием перед членом идентификатора интер- фейса с точкой. При этом, доступ к члену не указывается. Обращение к такому члену выполняется через преобразование объекта к требуемому интерфейсу, напри- мер: class MyClass : MyInterface1, MyInterface2 { int MyInterface1.Method() // Реализация метода MyInterface1 88 { return 1; } int MyInterface2.Method() // Реализация метода MyInterface2 { return 2; } } MyClass cl = new MyClass(); int i1 = ((MyInterface1)cl).Method(); // i1 = 1 int i2 = ((MyInterface2)cl).Method(); // i2 = 2 Явное указание интерфейса при реализации требуется и в том случае, если в двух интерфейсах используется свойство и метод с одинаковым именем, например: interface MyInterface1 { int A(); } interface MyInterface2 { int A { get; } } class MyClass : MyInterface1, MyInterface2 { public int A() { return 1; } int MyInterface2.A { get { return 2; } } } MyClass cl = new MyClass(); int i1 = cl.A(); // i1 = 1 int i2 = ((MyInterface2)cl).A; // i2 = 2 Пример: создание интерфейсов для двумерных (метод расчёта периметра) и трёхмерных фигур (метод расчёта объёма), добавление к классам Square и Rectangle интерфейса двумерной фигуры и создание класса «Куб» с интерфейсом трёхмерной фигуры и дополнительной координатой по оси Z interface I2D { double Perimeter(); } 89 interface I3D { double Capacity(); } abstract 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 abstract double Area(); } class Square : Figure, I2D { private double _size; public double size { get { return _size; } set { if (value > 0) _size = value; } } public Square() : base("Квадрат") 90 { _size = 1; } public Square(Square sourceSquare) : base(sourceSquare) { _size = sourceSquare._size; } public override double Area() { return _size*_size; } public double Perimeter() { return _size*4; } } class Rectangle : Figure, I2D { 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; } public override double Area() { return _size1*_size2; } public double Perimeter() { return (_size1+_size2)*2; } } class Cube : Figure, I3D { private int _posZ; private double _size; 91 public int posZ { get { return _posZ; } set { if (value >= 0) _posZ = value;} } public double size { get { return _size; } set { if (value > 0) _size = value; } } public Cube() : base("Куб") { _posZ = 1; _size = 3; // Просто для разницы начальных значений классов } public Cube(Cube sourceCube) : base(sourceCube) { _posZ = sourceCube._posZ; _size = sourceCube._size; } public override double Area() { return _size*_size*6; } public double Capacity() { return _size*_size*_size; } } Figure[] mas = new Figure[???]; string[] s = new string[mas.Length]; for (int i=0; i // s[0] = "Квадрат. Площадь: 1,00. Периметр: 4,00." // s[1] = "Прямоугольник. Площадь: 4,00. Периметр: 8,00." // s[2] = "Куб. Площадь: 54,00. Объем: 27,00." // s[3] = "XXXXXXXXXXXX. Площадь: X,XX." // ... 92 6 Делегаты, лямбда-выражения и события 6.1 Делегаты Делегаты представляют собой объекты, которые могут ссылаться на метод и вызывать его. При этом, имеется возможность менять метод, на который ссылается делегат, т.е. использовать один и тот же объект-делегат для вызова разных методов. Формальная схема описания делегата имеет вид: delegate <возвращаемый тип> <имя делегата>([<список параметров>]); Объявление делегата и присвоение ему метода осуществляется по схеме <тип делегата> <идентификатор> = new <тип делегата>(<метод>); или по упрощённой схеме (групповое преобразование методов) <тип делегата> <идентификатор> = <метод>; Пример: создать делегат, обрабатывающий целое число определённым обра- зом. Создать класс, имеющий методы добавления и умножения исходного числа на два. Произвести обработку числа. delegate int IntProcessing(int res); public static class MyClass { public static int Add2(int res) { return res + 2; } public static int Mul2(int res) { return res * 2; } } int i = 5; IntProcessing IntProc = MyClass.Add2; i = IntProc(i); // i = 7 IntProc = MyClass.Mul2; i = IntProc(i); // i = 14 Применение упрощённой схемы позволяет группировать методы, т.е. при вы- зове делегата выполнять несколько методов одновременно. Это достигается опера- циями сложения и вычитания методов. При этом, операции выполняются в порядке их добавления. Например, предыдущий пример (его основная программа) может быть реализован следующим образом: 93 int i = 5; IntProcessing IntProc = MyClass.Add2; IntProc += MyClass.Mul2; i = IntProc(i); // i = 10 Результат получился не тем, что ожидалось, т.к. каждому из методов было пе- редано одно и то же начальное значение переменной i и, фактически, это результат работы последнего метода. Для получения нужного результата изменим делегат и методы следующим образом: delegate void IntProcessing(ref int res); public static class MyClass { public static void Add2(ref int res) { res += 2; } public static void Mul2(ref int res) { res *= 2; } } int i = 5; IntProcessing IntProc = MyClass.Add2; int i1, i2, i3, i4; i1 = i2 = i3 = i4 = i; IntProc(ref i1); // i1 = 7 {5 + 2} IntProc += MyClass.Mul2; IntProc(ref i2); // i2 = 14 {(5 + 2) * 2} IntProc -= MyClass.Add2; IntProc(ref i3); // i3 = 10 {5 * 2} IntProc += MyClass.Add2; IntProc(ref i4); // i4 = 12 {(5 * 2) + 2} Попытка удалить из делегата метод, который ему не присвоен, не вызовет ни- каких действий. Делегаты становятся ещё более гибкими средствами программирования бла- годаря двум свойствам: ковариантности и контравариантности. Как правило, метод, передаваемый делегату, должен иметь такой же возвращаемый тип и сигна- туру, как и делегат. Но в отношении производных типов это правило оказывается не таким строгим благодаря ковариантности и контравариантности. Ковариантность позволяет присвоить делегату метод, возвращаемым типом которого служит класс, производный от класса, указываемого в возвращаемом типе делегата. Контравариантность позволяет присвоить делегату метод, типом параметра которого служит класс, являющийся базовым для класса, указываемого в объявле- нии делегата. 94 Такие присвоения возможны из-за того, что классу-родителю всегда можно присвоить класс-потомок. Следующий пример иллюстрирует применение принципов ковариантности и контравариантности. class MyClass1 { public int A, B; } class MyClass2 : MyClass1 { public int C, D; } static class Change { public static MyClass1 Change1(MyClass1 Arg) { MyClass1 Temp = new MyClass1(); Temp.A = Arg.B; Temp.B = Arg.A; return Temp; } public static MyClass2 Change2(MyClass2 Arg) { MyClass2 Temp = new MyClass2(); Temp.A = Arg.A; Temp.B = Arg.B; Temp.C = Arg.D; Temp.D = Arg.C; return Temp; } public static string GetS(MyClass1 temp) { if (temp is MyClass2) { MyClass2 cl2 = temp as MyClass2; return "MyClass2, A=" + cl2.A + ", B=" + cl2.B + ", C=" + cl2.C + ", D=" + cl2.D; } else return "MyClass1, A=" + temp.A + ", B=" + temp.B; } } delegate MyClass1 RunChange(MyClass2 Ard); MyClass2 cl = new MyClass2() { A = 1, B = 2, C = 3, D = 4 }; RunChange RC = Change.Change1; // контравариантность MyClass1 temp = RC(cl); string s = Change.GetS(temp); // s = "MyClass1, A=2, B=1" RC = Change.Change2; // ковариантность temp = RC(cl); s = Change.GetS(temp); // s = "MyClass2, A=1, B=2, C=4, D=3" 95 6.2 Анонимные методы и лямбда-выражения Метод, на который ссылается делегат, нередко используется только для этой цели. Иными словами, единственным основанием для существования метода служит то обстоятельство, что он может быть вызван посредством делегата, но сам он не вызывается вообще. В подобных случаях можно воспользоваться анонимной функ- цией, чтобы не создавать отдельный метод. Анонимная функция, по существу, представляет собой безымянный кодовый блок, передаваемый конструктору делега- та. Начиная с версии 3.0, в С# предусмотрены две разновидности анонимных функций: анонимные методы и лямбда-выражения. Для создания анонимного метода достаточно указать кодовый блок после ключевого слова delegate (или скобок с параметрами, если параметры имеются). Кодовый блок указывается в фигурных скобках, после которых ставится точка с запятой. Например: delegate int CalcSum(int start, int end); CalcSum Sum = delegate(int start, int end) { int sum = 0; for (int i = start; i <= end; i++) sum += i; return sum; }; int i1 = Sum(5, 8); // i1 = 26 int i2 = Sum(1, 3); // i2 = 6 В лямбда-выражениях применяется новый лямбда-оператор => , который раз- деляет лямбда-выражение на две части. В левой его части указывается входной па- раметр (или несколько параметров), а в правой части – тело лямбда-выражения. Если тело лямбда-выражения состоит из одного оператора, то формальная за- пись лямбда-выражения имеет вид: (<список параметров>) => <оператор>; при этом результатом лямбда-выражения будет результат выполнения оператора. Формальная запись <списка параметров> имеет вид: [<тип параметра 1>] <параметр 1>[,[<тип параметра 2>]<параметр2>,...] В большинстве случаев типы параметров не указываются. Кроме того, если имеется только один параметр, то его не обязательно заключать в круглые скобки. Например, используем лямбда-выражение для создания делегата, умножаю- щего целое число на два. delegate int Mul2(int value); 96 Mul2 Func = value => value * 2; int i = Func(5); // i = 10; Если тело лямбда-выражения состоит из нескольких операторов, то оно за- ключается в фигурные скобки, после которых ставится точка с запятой. Например, приведённый ранее пример с анонимными методами может быть записан с использованием лямбда-выражений следующим образом: delegate int CalcSum(int start, int end); CalcSum Sum = (start, end) => { int sum = 0; for (int i = start; i <= end; i++) sum += i; return sum; }; int i1 = Sum(5, 8); // i1 = 26; int i2 = Sum(1, 3); // i2 = 6 Как видно из примера, в теле блочного лямбда-выражения требуется исполь- зовать оператор return для указания результата работы. |