субота, 1 вересня 2012 р.

C#. Концепція та синтаксис. Частина 4


ДОДАТКОВІ МОЖЛИВОСТІ C#. 

Вказівники та Незахищений код
Змінна, яка представляє клас або масив, містить адресу пам'яті, у якій зберігається об'єкт (дані екземпляра). Це посилання синтаксично трактується так, немов змінна сама безпосередньо зберігає дані об'єкта. І лише через посилання можна отримати ці дані. Посилання C# розроблені так, щоб спростити код і мінімізувати можливість внесення помилок несвідомого псування даних у пам'яті.
В окремих випадках виникає потреба безпосередньої роботи з пам'яттю з допомогою покажчиків, добре відомих у C++ та інших алгоритмічних мовах. Цю функціональність можна викорис­товувати для забезпечення високої продуктивності окремих фрагментів коду або для звертання до функцій у зовнішній (не .NET) DLL, які вимагають передавання вказівника як параметра (наприклад, функції Windows API).
C# дає змогу використовувати вказівники лише у спеціальних блоках, які помічаються як незахищені (небезпечні) за допомогою ключового слова unsafe:
unsafe class C { / / довільний метод класу може використовувати вказівник
}
unsafe void M() {
//метод може використовувати вказівники
}
class A {
unsafe int *p //оголошення поля-вказівника у класі
}
unsafe {
/ /незахищений код
}
Не можна оголосити локальну змінну як unsafe. Якщо така потреба виникає, то цю змінну потрібно розмістити всередині незахищеного блоку.
Компілятор C# не буде компілювати код, який містить вказівники за межами блоків unsafe. Для використання режиму unsafe проект повинен містити увімкнену опцію Project І Properties | Build | Allow Unsafe Code.
Для оголошення вказівників використовують символ * :
int *pX, pY; double *pResult; void *pV;
На відміну від C++ символ * діє на всі оголошувані у стрічці змінні. Тобто pY також буде вказівником.
Для роботи з вказівниками використовують дві унарні операції:
        адресна операція & перетворює тип даних за значенням у вказівник (наприклад, int у *int);
        операція розіменування * перетворює вказівник у тип даних за значенням (наприклад, *int у int).
Розглянемо код:
int X = 0; int *pX; pX = &X; *pX = 10;
Оскільки pX містить адресу змінної X (після виконання оператора pX = &X), то код *pX = 10 запише значення 10 на місце X. Тобто в результаті змінна X набуде значення 10.
Вказівник можна привести до цілочисельного типу:
uint ui = (uint)pX;
Вказівники гарантовано можна привести лише до типів uint, long або ulong, а для 64-розрядних процесорів - лише до типу ulong.
Вказівник можна утворити лише на типи за значенням. Причому для структур існує обмеження: структура не повинна містити типів за посиланням.
Означимо наступну структуру:
struct Complex { public double Re; public double Im;
}
Ініціалізуємо вказівник на цю структуру:
Complex *pComplex;
Complex complex = new Complex();
*pComplex = &complex;
Доступ до членів структури можна здійснити за допомогою вказівника:
(*pComplex).Re = 1;
Однак такий синтаксис дещо ускладнений. Отож C# передбачає іншу операцію доступу до членів структури через вказівник:
pComplex->Re = 1;
У C# неможливо утворити вказівник на клас, однак можна утворити вказівники на члени класу, які мають тип за значенням. Це вимагає використання спеціального синтаксису з огляду на особливості механізму прибирання „сміття". У довільний момент часу може бути прийняте рішення про переміщення об'єктів класу на нове місце з метою упорядкування динамічної пам'яті. Оскільки члени класу розташовані в динамічній пам'яті, вони також будуть переміщені. А якщо на них були утворені вказівники, то з цього моменту їх значення стануть некоректними.
Щоб уникнути цієї проблеми, використовують ключове слово fixed, яке повідомляє прибиральника „сміття" про можливе існування вказівників на деякі члени окремих екземплярів класу. У цьому випадку такі об'єкти переміщатися в пам'яті не будуть.
Перепишемо структуру Complex як клас:
public class Complex { public double Re; public double Im;
}
Синтаксис використання fixed у випадку одного вказівника такий:
Complex complex = new Complex();
fixed (double *pRe = &(complex.Re))
{ ... }
Область видимості вказівника pRe розповсюджується лише на блок у фігурних дужках. Доки виконується код усередині блоку fixed, прибиральник „сміття" не чіпатиме об'єкт complex.
Якщо потрібно оголосити декілька таких вказівників, то всі вони описуються як fixed до блоку використання:
fixed (double *pRe = &(complex.Re))
fixed (double *pIm = &(complex.Im))
{ ... }
Якщо змінні однотипні, їх можна ініціалізувати всередині одного fixed:
fixed (double *pRe = &(complex.Re), double *pIm = &(complex.Im))
{ ... }
Блоки fixed можуть бути вкладені один в інший.
Вказівники можуть показувати на поля в одному і тому ж екземплярі класу, у різних екземплярах або на статичні поля, які існують незалежно від екземплярів класу.
До вказівників можна додавати та віднімати цілочисельні значення. У цьому випадку вказівник змінює своє значення на відповідне ціле число, помножене на довжину типу в байтах. Якщо додається число X до вказівника на тип T зі значенням P, то в результаті вказівник міститиме адресу P + X*(sizeof(T)).
З вказівниками можна використовувати операції +, -, +=, -=, ++ та —, де змінна з правого боку цих операторів буде long або ulong.
Можна віднімати вказівники на один і той же тип даних. Результатом такої операції буде різниця значень вказівників, поділена на довжину типу.
Для демонстрації арифметичних операцій над вказівниками утворимо високопродуктивний одномірний масив.
Усі масиви C# є об' єктами за посиланням і розміщуються у динамічній пам'яті. Процес вибірки з цієї пам'яті, запису в неї та її обслуговування є доволі об'ємним. Якщо є потреба утворити масив на короткий проміжок часу без втрат продуктивності через розташування в динамічній пам'яті, доцільно виконати це у стеку.
Для виділення деякої кількості пам'яті у стеку можна використати ключове слово stackalloc. Ця команда використовує два параметри: тип змінної, яку потрібно зберігати, і кількість змінних. Утворимо з її допомогою масив з n елементів типу double:
int n = 20;
double *pDoubles = stackalloc double[n];
У результаті виконання цього коду середовище виконання .NET виділить 160 байт (20*sizeof(double)) і запише у pDoubles адресу першого з них. Наступний код демонструє механізм доступу до елементів масиву:
*pDoubles = 0; //0-ий елемент
int k = 10;
*(pDoubles+k) = 1; //k-ий елемент
C# дає також альтернативний синтаксис доступу до елементів масиву. Якщо деяка змінна p має тип вказівника, а k є довільним числовим типом, то вираз p[k] завжди інтерпретується як *(p+k). Наприклад, останню стрічку коду можна записати так:
pDoubles[k] = 5;
Зазначимо, що, на відміну від звичайних масивів, ця стрічка не ініціює виняток, якщо k буде більшим за 19, тобто відбудеться вихід за межі масиву. Інформація у відповідних байтах буде затерта новим значенням. І найкращий випадок у цій ситуації - виникнення винятку в тій частині коду, де цю інформацію використовують. У найгіршому випадку отримаємо правдоподібні, проте невірні результати. Недарма такий код необхідно свідомо оголосити небезпечним.
Делегати подібні до вказівників на функції. їх можна використати для виклику різноманітних функцій з однаковою сигнатурою під час виконання програми. Сигнатура функції - це список типів параметрів і результату. Синтаксис оголошення делегатів подібний до оголошення функції з доданим ключовим словом delegate:
[модифікатори] delegate тип_результату
НазваДелегата( [параметри])
Для прикладу, схематично розглянемо клас:
public class ClassA {
public static double Ml (int i) {...; } public double M2(int i) {...;}
}
Методи M1 та M2 мають однакову сигнатуру. Опишемо делегата для цих функцій:
public delegate double DelegateM(int i);
Тепер наведемо приклад використання делегата:
DelegateM delegateM = new DelegateM(ClassA.M1);
double m1 = delegateM(10);
ClassA A = new ClassA();
delegateM = new DelegateM(A.M2);
double m2 = delegateM(10);
Зверніть увагу на схожість делегата на вказівник. У першій стрічці делегату надається адреса статичного методу M1 (назві методу передує назва класу). Тому в другій стрічці активізація delegateM (10) ініціює власне виклик методу ClassA.M1.
Далі код delegateM = new DelegateM(A.M2) надає делегату адресу методу M2 об' єкта A (назві методу передує назва об'єкта). Тому в останній стрічці активізація delegateM(10) ініціює виклик методу A.M2.
Зауважимо, що методи, на які посилається делегат, не обов' язково повинні належати одному класу.
З допомогою делегата можна викликати декілька методів. При цьому на делегата та методи накладається додаткове обмеження: і методи, і делегат повинні повертати тип void.
Для прикладу розглянемо такий код:
public delegate void DelegateM(int i);
public class ClassA {
public static void M1(int i) {...;} public void M2 (int i) {...; }
}
// тут деякий код
ClassA A = new ClassA();
DelegateM delegateM = new
DelegateM(ClassA.M1);
delegateM += new DelegateM(A.M2);

delegateM(10);
Як бачимо, об'єкти-делегати можна додавати, а також віднімати. Тобто застосовувати до них оператори +, -, +=, -=.
Виклик делегата delegateM(10) ініціює послідовні виклики всіх належних йому методів (ClassA.M1 та A.M2), параметром яких у нашому випадку буде число 10.
У попередніх прикладах проілюстровано механізм утворення та використання делегатів. Однак не зовсім очевидна доцільність використання делегатів, оскільки з тим самим результатом можна було б обмежитися простим викликом двох методів ClassA.M1(10) та A.M2(10).
Наведемо простий приклад корисності використання делегатів.
Наша задача полягає в написанні функції, яка повертає максимальний елемент із заданого одновимірного масиву. Причому бажано передбачити використання цієї функції для масивів довільного типу. Алгоритм пошуку максимального елемента простий: пройти всі індекси масиву, і на кожній ітерації шляхом порівняння між поточним елементом і досягнутим рекордом обирати новий рекорд. Однак тут виникає проблема: ми не знаємо нічого про тип елементів, отож не вміємо їх порівнювати.
Вирішимо цю проблему таким способом. Оскільки клієнтському коду відомо, з яким типом даних він хоче працювати, він може передати цю інформацію (правило порівняння елементів) функції пошуку максимуму через делегата. Означимо цього делегата так:
public delegate bool Compare(object obj1,object obj2);
Необхідно, щоб функція, на яку посилається делегат, повертала значення true, якщо перший аргумент більший за другий, і false - у протилежному випадку.
Метод Max матиме таку реалізацію:
static public object Max(
object[] objs, Compare cmp) { if (objs.Length == 0)
return null; else {
object record = objs[0]; for (int i = 1; i < objs.Length; i++) if (cmp(objs[i], record)) record = objs[i]; return record;
}
}
Раніше нами означено демонстраційний клас множини точок Points. Вважатимемо більшим той об'єкт Points, у якого довжина є більшою. Додамо до класу Points метод порівняння:
public static bool PointsCompare(
object l, object r) { Points L = (Points)l; Points R = (Points)r;
return L.GetLength() > R.GetLength() ?true : false;
}
Тепер усе готово для пошуку найбільшого об'єкта класу Points методом Max:
int k = 10;
Points[] points = new Points[k]; //тут утворюються та ініціалізуються //об'єкти масиву points //...
//готуємо параметр для методу Max
Compare delegateCmp= new
Compare(Points.PointsCompare);
//і одержуємо максимальний елемент
Points Ps = (Points)Max(points, delegateCmp);

Події дають змогу одному об'єкту інформувати інші про те, що щось відбулося. Наприклад, при натисканні клавіші на клавіатурі або миші Windows генерує подію, інформацію про яку при бажанні можуть отримати зацікавлені об'єкти.
Подію оголошують з допомогою ключового слова event: [модифікатори] event ім'я-класу делегата НазваПодії
За домовленістю імена подій розпочинаються префіксом On: OnClick, OnMouseDown і т.п. При генеруванні події потрібно активізувати метод, який представляє делегат.
Наведемо простий приклад генерування події виходу значення цілочисельної змінної за межі деякого інтервалу.
Означимо клас делегата:
public delegate void RangeOutHandler( object sender, RangeOutEventArgs e )
Усі делегати, які активізують код обробки подій, повинні повертати значення типу void та приймати два параметри. Перший параметр має тип object і представляє об'єкт, який згенерував подію. Другий параметр - це об' єкт класу, успадкованого від класу System.EventArgs. В означенні делегата ми використали клас RangeOutEventArgs, який оголосимо так:
public class RangeOutEventArgs: EventArgs { private string message;
public RangeOutEventArgs(string message) { this.message = message;
}
public string Message { get { return message;}
}
}
Тепер означимо клас, який може генерувати події:
public class RangeControl { private int value; private int left; private int right; //оголошення класу делегата public delegate void RangeOutHandler(
object sender, RangeOutEventArgs e); //оголошення події OnRangeOut public event RangeOutHandler OnRangeOut; //конструктор класу
public RangeControl(int value, int left, int right) {
this.left = left; this.right = right; Value = value;
}
//властивість для встановлення значення value public int Value { set {
this.value = value;
if (value < left || value > right)
//генеруємо подію виходу за межі діапазону OnRangeOut(this,
new RangeOutEventArgs("Вихід за межі!"));
}
}
}
В оголошенні класу означені делегат RangeOutHandler та подія OnRangeOut. Властивість Value контролює значення для змінної value на предмет виходу за межі діапазону [left,right]. Якщо відбувся вихід за межі діапазону, то утворюємо об'єкт класу RangeOutEventArgs, який разом з об'єктом класу RangeControl передаємо як параметр події OnRangeOut.
Зауважимо, що подія OnRangeOut має тип класу-делегата RangeOutHandler, який відповідає обмеженням багатоадресних делегатів (тип результату - void). Тобто делегат може представляти декілька методів. Це дає змогу клієнтському коду реєструвати необмежену кількість методів обробки події
OnRangeOut.
Генерування подій - не така розповсюджена практика, як перехоплення та обробка повідомлень. Що стосується користувацького інтерфейсу, то Microsoft вже написала всі генератори подій, які можуть вам знадобитися (вони розміщені у просторі імен Windows . Forms).
Щоб отримати подію у клієнтському коді, достатньо лише повідомити про це екземпляр класу RangeControl та передати йому інформацію про обробника цієї події:
//ця функція буде обробляти подію protected void UserHandler(
object sender, RangeOutEventArgs e) { MessageBox.Show(e.Message) ;
}
//тут деякий код
//утворюємо екземпляр класу RangeControl RangeControl rc = new RangeControl(1, 0, 5); //додаємо новий метод для обробки події rc.OnRangeOut += new
RangeControl.RangeOutHandler(UserHandler) ; //ініціюємо генерування події rc.Value = 6;
У результаті виконання цього коду буде згенерована подія OnRangeOut, і функція UserHandler виведе на екран діалогову форму з повідомленням „Вихід за межі".
Загальні типи (generics) дають змогу при оголошенні класів, структур, методів, інтерфейсів і делегатів не вказувати конкретні типи параметрів. Тип параметра визначить компілятор самостійно в момент оголошення змінної.

Загальний тип подібний до шаблону (template) C++, однак має деякі обмеження.
Утворимо клас з використанням загальних типів.
class CGeneric <TYPE1, TYPE2> { public TYPE1 Field1; public TYPE2 Field2;
public CGeneric (TYPE1 Field1, TYPE2 Field2) { this.Field1 = Field1; this.Field2 = Field2;
}
//інші члени класу
}
Після назви класу розміщено перелік загальних типів, які використовує клас. При оголошенні змінної типу цього класу замість загальних потрібно вказати конкретні типи. Наприклад:
CGeneric<int,int> intVar =
new CGeneric<int,int>(0,1);
CGeneric<bool,bool> boolVar =
new CGeneric< bool,bool >(true,false);
CGeneric<string,float> mixedVar =
new CGeneric<string,float>("Pi",3.14) ;
Компілятор читає конкретні типи та підставляє їх замість загальних. Наприклад, змінна intVar буде екземпляром класу CGeneric, де поля Field1 та Field2 мають тип int. У цьому випадку можна написати, наприклад, такий код:
int sum = intVar.Field1 + intVar.Field2;
Однак помилковим буде код
float sum = mixedVar.Field1+mixedVar.Field2;
оскільки операція додавання стрічки до числа неозначена.
У межах класу конкретний тип невідомий. Отож потрібно бути обережним з використанням специфічної функціональності конкретних типів і складанням виразів.
Загальні типи корисні за необхідності утворення класів з ідентичною функціональністю, проте різними типами даних. Як приклад, утворимо простий клас, який вміє шукати мінімум і максимум двох значень типу (з означеною операцією порівняння).
class CMinMax <TYPE> { public TYPE Field1; public TYPE Field2;
public CMinMax (TYPE Field1, TYPE Field2) { this.Field1 = Field1; this.Field2 = Field2;
}
public TYPE Min{
get { return Field1 < Field2 ?
Field1 : Field2; }
}
public TYPE Max{
get { return Field1 < Field2 ?
Field2 : Field1; }
}
}
Тепер використаємо клас CMinMax: //оголошення
CMinMax<int> iVar = new CMinMax<int>(0,1); CMinMax<float> fVar = new
CMinMax<float>(0.5,1.71) ; CMinMax<string> sVar = new
CMinMax<string>("Ab","Cd");
//використання int i = iVar.Min; // i = 0 float f = fVar.Max; // f = 1.71 string s = sVar.Min; // s = "Ab"
Очевидно, що для пошуку мінімуму-максимуму двох значень простіше використати безпосередньо оператор ? або статичні функції Min та Max класу Math. Наведений приклад демонструє лише принцип використання загальних типів.
Загальні типи можуть бути корисними і в тих випадках, де задля узагальнення деякої функціональності використовують тип object , а в кожному конкретному випадку він явним чином приводиться до потрібного типу. Оскільки компілятор замість загального типу підставляє заданий, то явного приведення типів можна уникнути. Це дає змогу писати продуктивніші програми. Окрім цього, на противагу використанню типу object з наступним приведенням типу, для загальних типів компілятор здійснює їхню перевірку у момент компіляції. Це зменшує можливість виникнення помилок виконання.
Параметризованими можна оголошувати і структури, інтерфейси та делегати:
//структура
public struct SGeneric <TYPE> { public TYPE Field;
}
//інтерфейс
public interface IGeneric <TYPE> { public TYPE Method();
}
//оголошення делегата
public delegate RESULTTYPE
DGeneric<RESULTTYPE,PARAMTYPE>
(PARAMTYPE aParam);
//використання делегата
DGeneric<double,int> dgen =
new DGeneric<double,int>(ClassA.M1);
double m1 = dgen(10);
Примітка. Загальні типи введені в .NET Framework 2.

Немає коментарів:

Дописати коментар