Сім чудес і два фокусу на Дельфі h2>
Максим Кузьмінський p>
вірите
Ви в дива чи ні, Ви, напевно, погодитеся зі мною, що іноді щось
таке трапляється з кодом наших програм, і вони раптом перестають компілюватися
або, що ще підступніше, починають видавати абсолютно непередбачуваний результат.
І ось тоді, ви згодні, вас починають відвідувати дивні думки про участь у
всі ці чудеса якихось потойбічних сил. p>
В
цій статті ми спробуємо сдернуть таємничий покрив з кількох, самих
простих "чудес" і переконаємося, що все це - лише обман, ілюзія, а
найчастіше - майстерне шахрайство. p>
Ми
розглянемо сім (з багатьох) таких чудес і спробуємо розгадати їх секрети. Зрозумівши
механізм їх походження, ми, в ув'язненні, покажемо два приклади використання
цих таємних сил у "мирних цілях". Наша мета - краще дізнатися Delphi і в
майбутньому уникнути деяких що важко помилок. p>
Для
того, що б ви зрозуміли, що я маю на увазі, давайте розглянемо один дуже
простий приклад. p>
Чудо
Перше (Round Miracle). P>
Відкрийте
Delphi, створіть новий проект, назвіть його AllMiracles, покладіть кнопку на
головну форму і напишіть в обробнику події OnClick наступний код: p>
procedure
TfrmAllMiracles.btnRoundMrclClick (Sender: TObject); p>
begin p>
ShowMessage (IntToStr (Round (3.5) --
Round (2.5))); p>
end; p>
Figure 1. p>
А
тепер зупиніться і скажіть, який результат ви очікуєте побачити. Я сподіваюся
ви не сказали "1", бо інакше це не було б диво. Ті, у кого добре
розвинута інтуїція, можуть сказати "0", і це буде ще далі від
правильної відповіді. І лише ті, хто часто грає в Спортлото або, у найгіршому
кінець, уважно читає документацію, відповість "2" і це буде
правильно. Не вірите? - Тисніть F9. P>
Читаємо Help по функції Round: p>
Round returns an Int64 value that is
the value of X rounded to the nearest whole number. If X is exactly halfway
between two whole numbers, the result is always the even number. p>
Ось
таке воно, "Кругле диво". p>
Сподіваюся,
тепер ви зрозуміли, про що ми будемо говорити сьогодні. У цій статті немає важких,
хитромудрих прикладів. Код - гранично спрощено що б виділити саму суть
проблеми. А наше з вами справа - розібратися в ній і, якщо можна, виправити
ситуацію. Як, наприклад, в наступному випадку. P>
Чудо
Друге (Absolute Miracle). P>
Покладіть
на головну форму створеного раніше проекту нову кнопку і напишіть в його
обробнику події OnClick такий код: p>
procedure
TfrmAllMiracles.btnAbsMrclClick (Sender: TObject); p>
var p>
i1: int64; p>
begin p>
i1: = abs (low (integer )); p>
ShowMessage (IntToStr (i1 )); p>
end; p>
Figure 2. p>
Перш
ніж натиснути F9, проаналізуємо написане. Low від integer - значення відоме
всім, записане навіть у Help'е і рівне -2147483648, тобто число негативне. p>
Help не говорить про функції Abs нічого нового: p>
Abs returns the absolute value of
the argument X. X is an integer-type or real-type expression. P>
Змінна
i1 описана як int64, і це правильно, тому що 2147483648 - вже виходить за
кордону типу integer. Це значення (2147483648) ми і очікуємо побачити на екрані,
чи не так? А ось і ні. Перевірте. На екрані знову - 2147483648. Як абсолютна
може мати негативне? p>
Давайте
ще раз, уважніше розглянемо вираз abs (low (integer)). Що можна ще
сказати про нього? Не дивлячись на наявність в ньому функцій, це - константа p>
Читаємо Help за темою "Constant expressions": p>
... Constant expressions cannot
include variables, pointers, or function calls, except calls to the following
predefined functions: Abs ... Low ... p>
спробуємо
описати константу із значенням рівним цього виразу: p>
... p>
const p>
ci = abs (low (integer )); p>
... p>
Figure
3. P>
Код
компілюється. Значить ми - праві, а це означає, що результат вираження
визначається ще на стадії компіляції. Далі, low (integer)) має цілий тип.
Abs від integer - теж ціле, а нам потрібно int64. Поробуем переписати код
наступним чином: p>
procedure TfrmAllMiracles.btnAbsMrclClick (Sender: TObject); p>
const p>
ci = abs (low (integer )); p>
var p>
i1: int64; p>
begin p>
// i1: = abs ((low (integer ))); p>
i1: = abs (int64 (low (integer ))); p>
ShowMessage (IntToStr (i1 )); p>
end; p>
Figure 4. p>
Тепер
- Запрацювало. Секрет "Абсолютного дива" розкритий! До речі,
abs (int64 (low (integer))) - теж константа. p>
Наступний
диво - приклад того, як цілком правильний код відмовляється компілюватися. p>
Чудо третє (One more low integer miracle). p>
Нова
кнопка на формі буде реагувати на натискання наступним чином: p>
procedure
TfrmAllMiracles.btnLowIntMrclClick (Sender: TObject); p>
var p>
lowInt: integer; p>
begin p>
lowInt: = -2147483648; p>
ShowMessageFmt ( '% d', [lowInt ]); p>
end; p>
Figure 4. p>
Цілком
звичайна процедура. У нас виникло бажання привласнити деякої змінної цілком
законне значення. Але цей код
НЕ компілюється: p>
Overflow in conversion or arithmetic
operation p>
Тиснемо
F1 на повідомленні про помилку і читаємо: p>
The compiler has detected an
overflow in an arithmetic expression: the result of the expression is too large
to be represented in 32 bits. p>
Мабуть
компілятор намагається визначити константи цілого типу зі значенням 2147483648, а
тільки потім змінити її знак, але це йому не вдається. Перепишемо код: p>
procedure
TfrmAllMiracles.btnLowIntMrclClick (Sender: TObject); p>
var p>
lowInt: integer; p>
begin p>
lowInt: =-int64 (2147483648); p>
// lowInt: = -2147483648; p>
ShowMessageFmt ( '% d', [lowInt ]); p>
end; p>
Figure 5. p>
Ось
тепер - все нормально. Приклад дуже простий, але дає нам уявлення про
те, як компілятор Delphi обробляє константи і визначає їх тип. p>
А
ось таке диво - приклад того, до якої плутанини може призвести перевантаження
функцій. Такі чудеса ми найчастіше самі влаштовуємо собі через неуважність, а
потім годинами шукаємо помилки. p>
Чудо
четверте (String Trick). p>
Ну,
що ж, додамо знову кнопку на нашу форму і задамо наступний код для події
OnClick: p>
procedure
TfrmAllMiracles.btnCopyMrclClick (Sender: TObject); p>
const p>
cs: array [0 .. 1] of char = '01 '; p>
begin p>
ShowMessage (copy (cs, 0,1) + copy (cs, 1,1 )); p>
end; p>
Figure 6. p>
Я
знаю, що ви вже чекаєте каверзи і все ж таки результат може виявитися несподіваним:
"00". P>
Як
звичайно звернемося до help'у, дивимося функцію Copy: p>
Returns a substring of a string or a
segment of a dynamic array. p>
... p>
function Copy (S; Index, Count:
Integer): string; p>
function Copy (S; Index, Count:
Integer): array; p>
... p>
Справа
в тому, що у виразі copy (cs, 0,1) + copy (cs, 1,1) обидва рази викликаються різні
версії функції copy, перший раз - для динамічних масивів, які нумеруються
з 0, а другий раз - для рядків, перший елемент яких має індекс 1. Обидва
рази cs перетвориться до необхідного типу, і те, що cs, як масив починається
з нульового елементу, в даному випадку не має ніякого значення. p>
А
тепер, нарешті, ми добралися і до об'єктів. Безліч Дельфійських чудес
пов'язані з тим, що об'єкти в Delphi - автоматично разименуемие посилання,
які можуть вказувати на звільнену або зайняту кимось іншим область
пам'яті. Про такі випадки написано чимало. Наше чудо - інше. P>
Чудо
п'ятий (Is-Miracle). p>
Опишіть
у розділі protected нашої форми поле FControl типу TСontrol і задайте для ще
одній - нової кнопки ось таку реакцію на її натискання: p>
procedure
TfrmAllMiracles.btnIsMrclClick (Sender: TObject); p>
begin p>
if (FControl is TControl) then p>
begin p>
if not Assigned (FControl) then p>
FControl: = TControl.Create (Self); p>
end p>
else p>
ShowMessage ( 'Not a Control'); p>
end; p>
Figure 7. p>
Таке
"Чудо" я бачив кілька разів і в різних проявах. Скільки раз б
ви не натискали на кнопку btnIsMrcl, ви кожного разу будете бачити повідомлення 'Not
a Control ', а конструктор TControl так ніколи і не буде викликаний. p>
Ось, що говорить Help: p>
... The expression object is class
returns True if object is an instance of the class denoted by class or one of
its descendants, and False otherwise. (If object is nil, the result is
False.) P>
Справа
в тому, що оператор is використовує посилання на клас об'єкта, а не те, як описана
змінна, яка по суті - простий покажчик. Так що TControl не завжди
TControl. P>
Так,
я сподіваюся ви розумієте, що TControl тут обрано випадково, з таким же успіхом
це міг бути і будь-який інший клас. p>
Випадок
коли FControl посилається на вже звільнений об'єкт або є локальної та
непроініціалізірованной змінної, дає непредказуемие результати і може
призвести до зовсім не чудовому краху аплікації. p>
А
от для наступного чуда я знайшов тільки непрямий обьяснение в Help'е і тому
ми будемо змушені провести невеликий експеримент. p>
Чудо шосте (Is-Miracle II) p>
Давайте
подивимося ще на одне, схоже диво пов'язане з оператором is. Додамо до нашої
групі проектів (ProjectGroup1) новий проект - DLL з ім'ям AllMirrLib, в
єдиному модулі якого буде наступний код: p>
library AllMirrLib; p>
uses p>
Controls; p>
function IsControlLib (const anObj:
TObject): boolean; p>
begin p>
Result: = anObj is TControl; p>
end; p>
exports p>
IsControlLib; p>
Figure
9. P>
Як
ви бачите ця бібліотека експортує тільки одну дуже просту функцію,
яка повертає знеченіе True в тому випадку, якщо її єдиний параметр
походить від TControl і False - в інших випадках. p>
В
модуль форми нашого основного проекту додамо наступне визначення: p>
unit AllMir; p>
interface p>
... p>
implementation p>
($ R *. DFM) p>
function IsControlLib (const anObj:
TObject): boolean; external 'AllMirrLib.DLL'; p>
Figure 10. p>
Тепер,
як завжди, додамо на форму нову кнопку: p>
procedure
TfrmAllMiracles.btnIsMrcl2Click (Sender: TObject); p>
begin p>
FControl: = TControl.Create (nil); p>
try p>
if not IsControlLib (FControl) then p>
ShowMessage ( 'Not a Control'); p>
finally p>
FreeAndNil (FControl); p>
end; p>
end; p>
Figure 11. p>
Як
ви вже напевно здогадалися FControl знову виявиться не TControl. Знайдіть у
модулі System процедуру _IsClass. Хоч вона й написана на асемблері, неважко
зрозуміти, що в ній відбувається - в циклі проглядаються посилання на класи
(спочатку власна - об'єкта, а потім - усіх предків) і серед них шукається
рівна правий операнд. Давайте трохи змінимо процедуру: p>
procedure TfrmAllMiracles.btnIsMrcl2Click (Sender: TObject); p>
var p>
p1, p2: pointer; p>
begin p>
FControl: = TControl.Create (nil); p>
try p>
p1: = pointer (FControl.ClassType); p>
p2: = pointer (TControl); p>
if not IsControlLib (FControl) then p>
ShowMessage ( 'Not a Control'); p>
finally p>
FreeAndNil (FControl); p>
end; p>
end; p>
Figure 12. p>
Подивіться
під відладчиком значення p1 і p2 - вони рівні. Тепер змінимо і функцію
IsControlLib: p>
TObject): boolean; p>
var p>
p3, p4: pointer; p>
begin p>
p3: = pointer (anObj.ClassType); p>
p4: = pointer (TControl); p>
Result: = anObj is TControl; p>
end; p>
Figure 13. p>
Тут
теж поставимо крапку зупинення і порівняємо значення. Змінні p1, p2 та p3 мають
одне і теж значення, а от p4 - вказує куди-то не туди. Проблема в тому, що
в аплікації і в DLL співіснують два різні класи TControl, ось тому
равества бути й не може. p>
Непряме
вказівка на цю проблему в Help'е можна знайти в описі методу ClassNameIs. p>
Читаємо Help: p>
Use ClassNameIs when writing
conditional code based on an object's type or to query objects across modules,
or DLLs. p>
Так,
до речі, не забудьте, що у вас два проекти в групі і компілюється завжди
тільки активний проект. Так що не забувайте перпеключаться на потрібний проект з
міру необхідності або компілюється відразу все: Alt-P, U. p>
Наступний
чудо я зустрів у програмі одного починаючого програміста і воно було звичайно
злегка закамуфльований, так що я, на свій сором, навіть не відразу зрозумів, у чому
справа. Я бачив значення змінних, знав, що це - змінні типу variant, але
ніяк не міг зрозуміти чому результат обчислення якогось нескладного вираження
весь час помилковий. Перевірте себе і ви. P>
Чудо
сьоме (Miracle with Variants). p>
Як
ви вже здогадалися, почнемо з нової кнопки, яка виконує наступні дії
при натисканні: p>
procedure
TfrmAllMiracles.btnVarMrclClick (Sender: TObject); p>
var p>
X, Y, Z: variant; p>
begin p>
X: = '1 '; p>
Y: = '2 '; p>
Z: = 3; p>
ShowMessage (X + Y + Z); p>
end; p>
Figure 14. p>
Можете
Чи ви передбачити результат виразу '1 '+ '2' 3? Якщо ви сказали '6 ', то ви
теж попалися. Подивимося уважніше, '1 '+ '2' буде ... звичайно '12 ',
12 +3 = 15. Це і є правильна відповідь. P>
Отже,
ми побачили сім чудес Delphi, сім - з багатьох. Це не означає, що вони --
яскраві або самі чудові. Але на них можна багато чому навчитися. Візьмемо останнє,
щойно розглянуте нами, чудо. Задумайтесь, як Delphi вдається зводити в
одному вираженні значення різних типів? А якщо один з членів виразу --
variant? p>
Фокус
перший (Variant trick) p>
Читаємо Help в розділі "Variants in expressions": p>
... In a binary operation, if only
one operand is a variant, the other is converted to a variant .. p>
Не
здається вам це дивним - variant можна складати з чим завгодно.
Наприклад, integer плюс variant - буде variant, а variant можна знову
складати з чим завгодно ... p>
Нова
кнопка на формі буде виконувати наступні дії: p>
procedure
TfrmAllMiracles.btnVarTrickClick (Sender: TObject); p>
var p>
v: variant; p>
b: boolean; p>
i: integer; p>
s: string; p>
d: TDatetime; p>
x: Double; p>
begin p>
v: = 0; p>
b: = true; p>
i: = 2; p>
s: = '3 '; p>
d: = StrToDateTime ('01/01/01'); p>
x: = 5; p>
v: = v + b + i + s + d + x; p>
ShowMessage (VarToStr (v )); p>
end; p>
Figure 15. p>
Не
здається вам, що чудо вже те, що цей код компілюється, але ж він ще й
видає якийсь результат. Адже все дуже просто - "variant можна
складати з чим завгодно "і знову отримаємо - variant. p>
Одного разу
до мене звернувся один мій знайомий з питанням чи немає в Delphi чогось подібного
прихованого параметру Self, але для оператора with. Ні - відповів я йому спершу, а
потім задумався ... p>
Фокус
другий (With-trick) p>
Припустимо
у нас є наступна функція: p>
procedure ShowText (sl: TStringList); p>
begin p>
ShowMessage (sl.text); p>
end; p>
Figure 16. p>
І
кнопка на формі: p>
procedure
TfrmAllMiracles.btnWithSelfTrickClick (Sender: TObject); p>
var p>
sl: TStringList; p>
begin p>
sl: = TStringList.Create; p>
try p>
sl.CommaText: =
'1, 2,3,4,5,6,7,8,9,0 '; p>
ShowText (sl); p>
finally p>
sl.Free; p>
end; p>
end; p>
Figure 17. p>
І
ми, з якихось причин, хочемо позбавитися від локальної змінної sl. Але для
того, що б звернутися до функції ShowText, ми повинні передати їй параметр типу
TStringList. Звідки ж його взяти? P>
Давайте
поміркуємо. Кожен метод отримує прихований параметр Self, може бути як-то
можна витягнути його звідти? Писати для цього спеціальний метод якогось класу
не хотілося б - адже це працювало б тільки для його нащадків. p>
Давайте
почитаємо Help, розділ
"TMethod type": p>
... This type can be used in a type
cast of a method pointer to access the code and data parts of the method
pointer ... p>
Не
чи це те, що ми шукаємо? p>
Визначимо тип
і функцію: p>
type p>
TSimpleMethod = procedure of object; p>
function GetWithSelf (const pr:
TSimpleMethod): TObject; p>
begin p>
Result: = TMethod (pr). Data; p>
end; p>
Figure 18. p>
Як
бачите, функція приймає покажчик на метод, а повертає об'єкт, що є
власником цього методу. Але яким же методом ми скористаємося? Наприклад, метод
Free, адже його історія сягає ще до самого TObject'у. Тепер перевіримо себе: p>
procedure
TfrmAllMiracles.btnWithSelfTrickClick (Sender: TObject); p>
begin p>
with TStringList.Create do p>
try p>
CommaText: = '1, 2,3,4,5,6,7,8,9,0 '; p>
ShowText (TStringList (GetWithSelf (Free ))); p>
finally p>
Free; p>
end; p>
end; p>
Figure 19. p>
перевірки
- Працює. P>