Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #135, МАРТ 2010 г.

Программерские типсы и триксы: потайные ходы в подземелье C#

Александр Эккерт (stannic.man@gmail.com)


Говорят, лень - двигатель прогресса. У тебя тоже есть свой двигатель прогресса - журнал "][", который посредством этой статьи поможет тебе сделать код на C# предметом зависти и восхищения собратьев по цеху :).

Сегодня я постарался рассмотреть некоторые малоизвестные широкой публике моменты, связанные с программированием на C#, которые помогут тебе избежать некоторых ошибок и сделать твои приложения гораздо более гибкими и адекватными.

Константы и поля только "для чтения"

В стандарте C# предусматриваются поля и константы readonly ("только для чтения"):

public readonly int ReadonlyValue = 1;
public const int ConstValue = 1;

В чем же между ними разница? Константы вычисляются на стадии компиляции программы, а значения "readonly" - только на стадии выполнения программы. Неочевидные последствия такого отличия могут проявиться в том случае, когда, скажем, библиотека и использующая ее программа компилируются отдельно. Если в библиотеке нужно использовать константу, то при изменении ее значения (перекомпиляции библиотеки), нужно будет пересобирать и саму программу. При использовании простых readonly-полей, перекомпилировать программу уже не нужно.

Оператор "IS" или метод IsSubclassOf()?

Какая разница при использовании оператора is и метода IsSubclassOf()? Ведь, казалось бы, оба они несут одну и ту же функциональность?
Во-первых, вызов оператора is проходит на порядок быстрее, поскольку он компилируется в простую инструкцию MSIL "asclass", тогда как вызов метода IsSubclassOf() проходит гораздо дольше. Оператор is может вызываться в случае, когда первый операнд равен "null", тогда как вызвать IsSubclassOf() у такого объекта не получится. И наконец, оператор is работает как с классами, так и с интерфейсами, в то время как IsSubclassOf() может работать только с классами.

Разбираемся с приведением типов

В C# существуют два вида приведения типов: использованием оператора "as" и прямое приведение. Практика показывает, что в коде гораздо предпочтительнее использовать прямое приведение типов и вот почему. Если система по какой-то причине откажется проглатывать прямое приведение типов, то будет сгенерировано исключение NullReferenceException, тогда как оператор as просто вернет null. Во втором случае существует большая вероятность, что после получения нулевой ссылки от оператора as, где-то глубоко в недрах твоей программы будет сгенерировано исключение NullReferenceException, отыскать причины которого будет уже не так-то просто. В случае прямого приведения типов причина ошибки будет очевидной.

Разница между полем и свойством в C#

Очень часто возникает вопрос: «а в чем разница между полем и свойством в C#»? С точки зрения доступа - обычное поле и свойство с доступом get/set ничем не отличаются. Поэтому, ответ на вопросы «что предпочтительней» и «как именно использовать» зависит от контекста программы.

Во-первых, использование свойства нужно, когда необходимо при установке свойству определенного значения, выполнить какие-то действия:

set { param1 = value; DoSomeWorkOnChanged(); }

Во-вторых, использование свойства требуется в том случае, когда нужно проверить присеваемое свойству значение:

set { if (value > 0) param1 = value; }.

Ну и в-третьих, использование свойства целесообразно, когда значение свойства не хранится в классе, а, например, считывается из базы данных:

get { return ReadFormDB("param1"); }
set { WriteToDB("param1", value); }

Перехват Win32-сообщений

Часто в твоем приложении возникает необходимость перехватить и использовать для своих нужд сообщения Win32. Как это сделать на C#? Очень легко - достаточно задействовать в своем коде интерфейс Windows.Forms.IMessageFilter. Параметры сообщения будут доступны в свойствах m.LParam и m.WParam:

Ловим Win32-сообщения

public class Win32MessageFilter:
System.Windows.Forms.IMessageFilter
{
public bool MessageFilter(ref Message m)
{
//клик левой кнопкой мыши
if(m.Msg == 513)
{
MessageBox.Show("Win32 message WM_LBUTTONDOWN");
return true;
}
return false;
}
}
static Win32MessageFilter filter =
new Win32MessageFilter();
static void Main()
{
Application.AddMessageFilter(filter);
Application.Run(new Form1() );
}

Недостаток этого перехвата в том, что тебе придется расшифровывать сообщения по их коду.

Защита от переполнения - ключевые слова checked и unchecked

В C# существуют такие ключевые слова - checked и unchecked, использование которых поможет предотвратить переполнение целого и повысить безопасность твоего кода. Можно объявить checked-блок:

Контролируем переполнение целого числа

byte a = 1;
bute b = 255;
checked
{
byte c = ( byte ) ( a + b );
byte d = Convert.ToByte( a + b );
Console.WriteLine(" { 0 } { 1 }", b + 1, c );
}

В данном случае, приведение a + b от int к byte приведет к исключению. В строке с Convert.ToByte, исключение возникло бы и без ключевого слова checked, но его наличие приводит к возникновению исключения еще и при вычислении аргументов метода Console.WriteLine(). Поскольку иногда переполнение целого числа допускается намеренно, то имеется ключевое слово unchecked, которое отключает контроль за переполнением. Ключевые слова checked и unchecked можно использовать для включения/отключения контроля в одном выражении:

checked ( c = ( byte ) ( b + a )).

Наконец, можно включить контроль за переполнением с помощью флага "/checked" компилятора - если этот флаг присутствует, то нужно явно помечать словом unchecked те участки кода, где переполнение допустимо.

Равенство и тождество объектов

Очень часто разработчикам приходится писать код для сравнения двух объектов (к примеру, при поиске, сортировке и сравнении отдельных элементов набора (массива). Справедливости ради надо сказать, что с определением равенства и тождественности в .NET Framework дела обстоят не совсем ладно.

У типа System.Object есть метод Equales, который возвращает true для двух "равных" объектов. Вот как выглядит стандартная реализация этого метода в типе Object:

Стандартная реализация метода Equals

public class Object
{
public virtual Boolean Equals( Object obj )
{
if( this == obj ) return true;
return false;
}
}

Код выглядит очень просто, однако возникает вопрос - а если аргументы ссылаются на разные объекты? В таком случае методу Equals гораздо сложнее определить, содержат ли объекты одинаковое значение. Иначе говоря, указанная выше реализация метода Equals у типа Object реализует проверку на тождество, но не равенство и поэтому никуда не годится. Поэтому сейчас мы напишем свой, грамотный вариант реализации метода Equals:

Грамотная реализация метода Equals

public class Object
{
public virtual Boolean Equals( Object obj )
{
if ( obj == null ) return false;
if ( this.GetType() != obj.GetType() )
return false;
return true;
}
}

Этот метод вполне может использоваться для переопределения метода Equals() в твоем коде для получения равенства типов. Для проверки на тождественность в .NET предусмотрен метод, и ты его, уверен, знаешь - Object.ReferenceEquals().

Немного об оптимизации кода

Существует два параметра компилятора C#, которые влияют на оптимизацию кода - /optimize и /debug. В приведенной таблице ты можешь увидеть варианты их использования - как говорится, "почувствуйте разницу"

Если твои эксперименты покажут, что JIT-компилятор CLR не обеспечивает нужной производительности, воспользуйся утилиткой ngen.exe, поставляемой в комплекте .NET Framework SDK. Она компилирует весь IL-код выбранной сборки в машинный код и сохраняет его на диске. При запуске сборки, CLR автоматически проверит наличие предварительно скомпилированной версии сборки, и, если она есть, загрузит ее, пропустив предварительную компиляцию.

К вопросу о потоках

Не могу не затронуть вопрос об эффективном использовании потоков в приложении - часто приходилось видеть, как из мощного оружия улучшения производительности, злоупотребления с потоками приводили к обратному результату. Если говорить точно, то использование потоков всегда приводит к издержкам в работе системы, потому что процессор тратит уйму времени и ресурсов при создании потоков и переключении контекста между ними. С появлением многопроцессорных (многоядерных) систем эта проблема отошла на второй план («правильно, а чего мелочиться с потоками, у меня четыре проца их проглотят и не подавятся ;)»), но тем не менее, грамотное использование поточных моделей в .NET-приложениях будет играть на руку разработчику, не говоря уже о росте его профессионализма.

В .NET существует готовое решение для управления собственным пулом потоков. Пул потоков позволяет разработчику найти золотую середину - с одной стороны, малое число потоков экономит ресурсы, а большое - позволяет со всем размахом воспользоваться преимуществами многопроцессорных систем; Пул потоков просто приспосабливается к текущей ситуации: если нужно выполнить много задач и в системе есть больше одного процессора, CLR создаст новые потоки. При уменьшении загруженности, количество потоков будет уменьшено.

И тут в руки программиста попадают методы регулирования их количества - SetMaxThreads и GetMinThreads. Как правило, практика показывает, что на работу приложения очень негативно влияет вызов функции SetMaxThreads, потому что манипуляции с максимальным количеством потоков ухудшают, а не улучшают производительность приложения. Если же тебе в приложении требуется больше 25 потоков на процессор, то следует задуматься об архитектуре программы и способах использования потоков в ней.

Если же тебе в приложении требуется больше 25 потоков на процессор, то следует задуматься об архитектуре программы и способах использования потоков в ней.

Вместе с этим, стоит отметить, что в CLR заложен механизм предотвращения слишком частного создания потоков - разрешается создавать не более одного потока в 500 мс. Если тебя такой вариант не устраивает, в приложение можно добавить вызов SetMinThreads, которому можно передать минимальное допустимое число потоков в пуле. Пул быстро создаст указанное число потоков, а есели при появлении в очереди новых заданий все потоки будут заняты, он продолжит создание потоков со скоростью не более одного потока в 500 мс.

Содержание