Урок информатики для мелкософта (пояснение к залепе №8)
Давайте поговорим о С++ и его синтаксисе. В частности о "сокращенной" записи некоторых операций. Слово "сокращенной" я взял в кавычки неспроста. Дело в том, что это сделано не только (и не столько) для того чтобы избавить пользователя от написания длинных строк. Хотя, учитывая майкрософтовскую маниакальную тягу к длинным идентификатором, эти сокращенные формы действительно дают прирост производительности программиста. :)
Ладно, шутки в сторону. Давайте о серьезном.
Язык С (да и его потомок - С++) спроектированы таким образом, чтобы (при использовании НОРМАЛЬНОГО компилятора) программист мог еще на этапе разработки программы управлять полученным машинным кодом. Удивлены? Я тоже не сразу поверил.
Язык С разрабатывался как альтернатива ассемблеру (по этому поводу заранее попрошу не флудить в комментах), т.к. серьезные программы писать на асме в то время стало уже слишком сложно. Другие языки высокого уровня генерировали слишком медленный и прожорливый машинный код и не позволяли программисту управлять этой самой генерацией. Выходило, что чтобы получить оптимальное по скорости и памяти решение (например драйвер устройства или часть ядра ОС) необходимо использовать только ассемблер, напороть ошибок в котором проще простого.
Собственно это и послужило причиной создания нового языка, который должен был отвечать определенным требованиям, примерно таким:
- был бы достаточно высокоуровневым, т.е. содержать конструкции высокоуровневых языков;
- был бы достаточно низкоуровневым, т.е. имел бы (насколько это возможно) команды, аналогичные типовым командам процессора;
- позволял бы получать машинный код, хотя бы сравнимый с ассемблерным вариантом;
- позволял бы программисту (хотя бы частично) управлять генерацией получаемого кода.
Именно исходя из этих требований, в языке С есть, например:
- команды инкремента/декремента (а++, а--), как аналоги соответствующих команд ЦП;
- возможность указать, что переменная должна храниться в регистре процессора, а не в памяти;
- работа с указателями;
- возможность управления размещением переменных и полей структур в памяти;
- команды выделения/освобождения памяти;
- команды поразрядных сдвигов;
- типы, сходные с аппаратными типами (поддерживаемыми процессорами);
- типы unsigned;
- возможность использования любого простого типа в качестве логического (bool);
- отсутствие встроенного строкового типа (т.к. такой тип не поддерживался ни одной платформой);
- и т.д и т.п.
Я слышу, как самые нетерпеливые читатели уже кричат, что многие языки поддерживают все это. Верно, поддерживают! Но это сейчас. А в то время таких возможностей не было ни в одном языке высокого уровня. Возьмите документацию по FORTRAN, COBOL, ALGOL...
Короче, С разрабатывался как полноценная, достаточно высокоуровневая (!) замена ассемблеру. Точка. Естественно и логично, что С++ унаследовал все эти возможности плюс добавил к ним ПОЛНОЦЕННУЮ поддержку ООП. Далее речь пойдет именно о С++.
Экскурс в историю окончен. Пришло, наконец, время объяснить, в чем же различия между полной и сокращенной записью команд и почему одно вовсе не соответствует второму.
Итак, рассмотрим механизм работы выражения c = a + b (естественно, что все три переменные имеют один и тот же тип и для данного типа определена операция сложения). Значение переменной a складывается с значением переменной b и результат заносится в переменную c. Вроде бы все просто. Но эта простота существует только до тех пор, пока наши переменные имеют простой тип. Давайте заменим их объектами и посмотрим, что получится.
А получится следующее:
- для объекта a будет вызван метод T operator+(const T&) const, которому в качестве параметра будет "скормлен" объект b;
- этот метод что-то сделает (например прибавит одно к другому) и создаст временный объект типа Т для хранения результата;
- затем метод вернет ссылку на созданный временный объект (назовем его "объектом D");
- будет вызван метод объекта с: T& operator=(const T&), т.е. ни что иное, как оператор присваивания;
- этот метод и приведет объект с в соответствие с состоянием объекта D, переданного в качестве аргумента.
- Временный объект D будет уничтожен.
Вот именно так работает вся эта, казалось бы простая, кухня. На что нам нужно обратить внимание в первую очередь?
Первоочередную важность тут имеют не вызовы методов и не махинации с объектами, а тот факт, что наше простейшее и казалось бы безобидное выражение c = a + b приводит к автоматическому (скрытому от наших глаз) созданию и уничтожению объекта со всеми вытекающими отсюда накладками!
Может показаться, что создание и уничтожение временного объекта-пустышки хоть и связано с лишней тратой системных ресурсов, но все же не так уж и страшно, особенно при теперешних-то мощностях. Но тут я хочу напомнить, что создание объекта - это не только выделение памяти под сам объект, но и создание и инициализация всех его членов-полей (которые, в свою очередь, тоже могут быть объектами), а также выполнение конструктора этого объекта и всех конструкторов всех его предков. Соответственно уничтожение объекта - это вызов деструкторов объекта и всех деструкторов всех его предков плюс уничтожение всех его членов-полей, которые тоже могут быть объектами. Получается не так уж и мало.
Ну, а если этот объект представляет собой, например, таблицу из базы данных, который, вдобавок ко всему сказанному, при инициализации будет устанавливать соединение с этой самой БД и загрузку данных из нее, отъедая не только память и процессорное время, но еще и канал связи? Подумайте, такими ли скромными могут оказаться на практике эти "накладные расходы".
Ладно, давайте перейдем к "сокращенной" записи описанной выше операции и подумаем вот над чем: теоретически методы operator+() и operator+=() должны делать одно и тоже, различие лишь в том, куда направится результат. Получается, что метод operator+=() можно "вывести" из метода operator+() (как и сделано в C#), но зачем же тогда создатели языка С++ сделали возможность перегрузки обоих методов для каждой операции? Только лишь для того, чтобы программист мог написать различные варианты, оптимизированные для обоих случаев? Это вряд ли.
Уверен, что дальше можно уже ничего не говорить, т.к. настоящие кодеры уже поняли к чему весь этот длинный пост и, вставляя трясущейся рукой сигарету в рот, судорожно прокручивают в мозгу все ужасные последствия применения этого "выведения сокращенного оператора из полного" по схеме "а'ля Шарп". Но все же я разжую ситуацию, дабы и не сильно понятливые осознали всю дикость такого "выведения" и поняли, что при словах "Шарп" и "Майкрософт" рукам есть отчего затрястись.
Итак, по образцу и подобию предыдущих абзацев, разложу все по полочкам. Берем выражение a += b и препарируем его:
- для объекта a будет вызван метод T& operator+=(const T&), которому в качестве параметра будет "скормлен" объект b;
- этот метод что-то сделает с объектом а (изменит его состояние ) и вернет ссылку на объект а.
Всё! Никаких созданий лишних объектов, никаких удалений, никаких присваиваний, ни-че-го! Надеюсь теперь понятно, почему не стоит в С++ делать такие "заглушки":
{ return this->operator=(this->operator+(b)); };
или
что одно и тоже.
Разница в потреблении ресурсов, конечно, впечатляющая, но вроде не такая сильная, чтобы заставить руки так сильно колотиться. Наверное я сгустил краски. Что ж, это действительно так... но только для С++. Теперь давайте рассмотрим, к чему приведет такая же (автоматически генерируемая компилятором) заглушка в шарпе.
Отличие шарпа от С++ (в данном случае) в том, что наши переменные a и b являются на самом деле не "хранителями" объекта, а всего лишь ссылками на него. Вроде бы разницы в результатах быть не должно, но если бы так было на самом деле, то в С++ создатели не вводили бы лишний тип - ссылки. Ладно, смотрим!
Для пояснения ситуации приведу кусок кода из "Залепы №8" (далее все написано на C#):
MyCls a = new MyCls();
MyCls b = new MyCls();
MyCls c = a; // (1)
a.x = 4; b.x = 6; // (2)
Console.WriteLine("Before: A={0}, B={1}, C={2}", a.x, b.x, c.x);
a += b; // (3)
Console.WriteLine("After: A={0}, B={1}, C={2}", a.x, b.x, c.x);
Итак, по-пунктам:
- в строке (1) переменная c становится "дублем" переменной a, т.к. теперь ссылается на тот же объект в памяти, на который ссылается a;
- после инициализации в строке (2), значение c.x так же как и значение a.x становится равным 4 (они же ссылаются на один и тот же объект)
- после выполнения сгенерированного компилятором оператора operator+=() происходит что-то очень странное - связь переменных a и c внезапно разрывается, ибо теперь они указывают на совершенно разные объекты! Это подтверждается вторым выводом значений переменных. Разве этого мы добивались командой сложения? Разве наше сложение не должно просто изменить объект, на который ссылается переменная a и всё???
А происходит такое именно из-за созданной компилятором описанной выше заглушки. Получается, что оператор сложения создал временный объект с результатом (помните "объект D"? ), ссылка на который и записалась в нашу переменную a. А переменная c об этом создании до сих пор ничего не знает и (что самое страшное) НИКОГДА не узнает, ибо для исправления этого "улучшительно-упрощающего" косяка никаких средств в языке нет.
Вот так и будет у нас болтаться в памяти фантом (на который ссылается переменная c), который должен быть давным-давно уничтожен, а правильнее - просто откорректирован еще в методе operator+=(), создание которого майкрософт возложило на компилятор и (внимание!) запретило программистам. Поэтому, если вам захочется самостоятельно перегрузить метод operator+=() или его аналоги для других операций, то просто попейте воды, сделайте несколько глубоких вдохов и расслабьтесь - вам перегрузить эти методы не удастся.
Ну что, Вам все еще хочется "самой перспективной технологии будущего"? Мне уже нет. :(
Залепа №8. Крах технологии от обычного int'а.
Все-таки вредно не спать по ночам! Лежал вот, думал. Пытался вникнуть в суть технологии, подстроиться как-то, понять то, что все еще понять не могу, приспособиться и начать (наконец!) получать удовольствие от ее использования. Ведь тысячи программистов пишут под .NET программы и вовсе не страдают от этого, не ощущают никаких неудобств. Вокруг только и слышно: "За .NET будущее", "Самая удобная система", "Передовое решение". Наверное все же я слишком тупой, что не вижу всех этих прелестей.
Вот такие мысли роились у меня в голове и нароили в конце-концов одну идею, которую я и решил тут же опробовать. Даже из постели вылез. На дворе пятый час ночи, а я тут .NET на прочность проверяю! :)
Как я и ожидал, технология мой простейший экзамен с треском провалила! Супер-простое приложение (30 строчек) сработало правильно, но АБСОЛЮТНО НЕЛОГИЧНО. Но, поскольку на дворе глубокая ночь, то описывать происходящее я не буду, просто приведу код программы. Пусть это послужит читателям этаким ребусом, разминкой для мозгов, домашним заданием.
Посмотрите программу, и решите, какой именно она выдаст результат. Потом откомпилируйте ее и сравните реальные ответы с теми, которые вы ожидали. Думаю будет интересно :)
using System;
using System.Collections.Generic;
using System.Text;
namespace GluckTest
{
class MyCls
{
public int x;
public static MyCls operator +(MyCls a, MyCls b)
{
MyCls rez = new MyCls();
rez.x = a.x + b.x;
return rez;
}
}
class Program
{
static void Main(string[] args)
{
MyCls a = new MyCls();
MyCls b = new MyCls();
MyCls c = a; // теперь А и С указывают на один и тот же объект
a.x = 4; b.x = 6;
Console.WriteLine("Before: A={0}, B={1}, C={2}", a.x, b.x, c.x);
a += b;
Console.WriteLine("After: A={0}, B={1}, C={2}", a.x, b.x, c.x);
Console.ReadLine();
}
}
}
Если Вы смогли предсказать верный ответ, то значит Вы очень неслабо разбираетесь в происходящем и дальше Вам читать нет смысла. У меня только один вопрос: считаете ли Вы такое поведение ЛОГИЧНЫМ и УДОБНЫМ для разработчика???
Кстати, в C++ чтобы добиться такого же "эффекта" надо быть оче-е-е-ень глупым программистом! Да и отловится он там моментально, как только вы уничтожите "лишний" объект. А вот в C# причиной является не программист, а именно сама технология и над исправлением этой аномалии придется поломать голову.
Майкрософт уже не один год продвигает .NET (и, конечно же C#) как "более легкую и удобную" среду для разработки приложений, которая "лишена недостатков C++". Приведенный пример, на мой взгляд, очень наглядно показывает, что в C# могут быть неудобства и пострашнее чем в С++.
Немного подумав, можно понять, что программа все же работает правильно и никаких глюков/ошибок тут нет. Но НЕ ДАЙ БОГ вам наступить на такие грабли в вашей реальной работе!
Мой пример, конечно, синтетический. Но давайте мысленно заменим класс MyCls на какой-нить нормальный класс с кучей полей, свойств, интерфейсов, закрытых и открытых методов, а так же "зароем" переопределение операции сложения куда-нибудь в пра-пра-пра-родителя этого класса. А теперь представьте, что Вам необходимо встроить этот класс в свой проект и сцепить его в плотный узел с десятками других классов и тысячами объектов....
Естественно результат работы Вашей программы будет сильно отличаться от ожидаемого и придется потратить массу времени и сил, чтобы разобраться в ситуации и понять в чем дело. И будет очень хорошо, если дружба с отладчиком и вырывание волос на голове прекратится менее чем за неделю. Иначе есть нехилый риск стать пациентом местной психиатрической клиники.
Залепа №7. Самые умные на свете.
Комплекс неполноценности майкрософта проявляется в самых разных формах. То они директоров школ на бабки разводят, то тортом в морду на глазах всего честного люда получают. Короче, используют всевозможные способы привлечь к себе внимание.
Ну и конечно же именно с ними должны все считаться, как с самыми авторитетными, самыми знающими и самыми прозорливыми. Вот об этом и поговорим. Точнее не о качествах конторы, а о качествах их продуктов, которое, как и качество всего остального, измеряется юзабельностью.
Итак, чем же мелкомягкие не угодили мне сегодня.
А сегодня я обнаружил, что оказывается, на многие действия пользователя софт-гигант уже предусмотрел свою, мудрую на их взгляд, реакцию. Нажал ли юзер кнопку, кликнул ли мышью - система ответит ему именно тем действием, которое он ожидает. В принципе это наверное здорово, кодеры должны быть благодарны, ведь часть их работы уже сделана. Но как все это соотносится с эргономикой?
А получается, что никак. Точнее не просто "никак", а скорее даже прямопротивоположно.
Например, у вас, как у любого нормального пользователя, уже в подсознании "зашито", что двойной клик приводит к редактированию или запуску. Но вот МС считает, что в дереве дабл-клик должен приводить к сворачиванию/разворачиванию узлов. Попытка переопределить событие DoubleClick на вызов окна редактора приводит к успеху - окно вызывается и работает, но после его закрытия снова срабатывает "стандартное" действие - сворачивание или разворачивание узла. Избавиться от этого "улучшения" я не смог.
Другой пример - DataGridView. Попытка перехватить событие нажатия клавиш так же срабатывает успешно, но если вы нажмете Enter, то после обработки события библиотека самозабвенно с милой улыбкой передвинет указатель текущей записи на следующую. Это поведение так же не лечится.
Кстати, Вы знаете, что DataGridView имеет свойство AutoGenerateColumns? Авторы учебников по C# о нем знают, в MSDN оно так же описано, даже IntelliSence его признает, но вот в окне Properties можете его не искать - мелкомягкие решили, что на этапе проектирования разработчику оно не понадобится, поэтому в "Свойствах" его нет.
Позже буду дополнять эту тему, т.к. я уверен, что описанное здесь - только вершина айсберга.