Заметки о C# часть 1

Когда рекомендуется использовать значимый тип вместо ссылочного

Структуры относятся к значимым типам в C#. Это означает, что память для них выделяется не в управляемой куче, а прямо в стеке. Это имеет свои плюсы и минусы. Вот некоторые условия, при выполнении которых стоит задуматься об использовании значимых типов, вместо ссылочных.

Также следует помнить о размерах экземплярных типов. Помня, что память выделяется в стеке, а сам стек не бесконечный. Если метод принимает значимый тип: все поля экземпляров значимых типов копируются, в то время как ссылочные типы передаются по ссылке. Значимые типы подвергаются упаковке и распаковке при работе с кучей. 

Как работать со значимыми типами без упаковки

Обобщенные классы <T>, например List<T>. Мало того, что API стал красивее, производительность выросла. Использование стало строго типизированным на этапе компиляции. Обобщенные классы имеют одну замечательную возможность: они могут работать с коллекциями значимых типов без упаковки/распаковки. 

Возможные ошибки при работе с упаковкой

Для многих программистов .NET операции упаковки (boxing) и распаковки (unboxing) не являются кристально понятными. Это те операции, которые вызываются неявно и требуют внимательного к себе отношения. Рассмотрим несколько примеров.

int a = 7;
object b = a; //тут происходит операция упаковки
int c = (int) b; //тут происходит операция распаковки

В этом простом примере можно наблюдать магию. Во второй строчке переменная a будет упакована и отправлена в управляемую кучу. Если при этом значение a изменится, в объекте b будет находится старое значение. На третьей строчке происходит операция распаковки. при этом копируется значение из упакованного в управляемой куче значения типа int (Int32) b.

Одна из неочевидных ошибок заключается в типизированной распаковке. Такая распаковка может сгенерировать исключение InvalidCastException. Рассмотрим код, которые скомпилируется, но сгенерирует вышеупомянутую ошибку на этапе исполнения.

int a = 5;
object b = a;
short c = (short)b; // Тут сгенерируется исключение

Другой пример ошибки:

double pi = 3.1415;
object obj = pi;
//int piInt = (int)obj; - так нельзя, будет ошибка
int piInt = (int)(double)obj; // выполнит операцию распаковки и приведения к типу

 Следует помнить правило: если нужна ссылка на экземпляр значимого типа, этот экземпляр должен быть упакован. Обычно упаковка производится, когда нужно передать значимый тип в метод, требующий ссылочный тип. Например:

void Print(object line)
{
  Console.WriteLine(line);
}

static void Main() { Print(4); //Неявная упаковка }

С другой стороны значимые типы намного легче и работа с ними происходит быстрее, чем со ссылочными типами. Каждый объект ссылочного типа хранит дополнительную информацию в виде индекса блока синхронизации. Также (во имя избежания ошибок) CLR неявно делает значимые типы запечатанными (sealed).

dynamic и var

С появлением ключевого слова dynamic множество операций стало выполнять проще. Например, работа с COM объектами, работа с Office Word и Excel (без многострочных конструкций). В чем же отличие?

var является синтаксическим сахаром. Ключевое слово может использоваться только внутри методов, позволяет компилятору автоматически указать тип переменной из правой части выражения. Dynamic же может использоваться как внутри методов, так и полями и аргументами. Для себя можно разделить их таким образом: var не тип, dynamic - тип (Type). var переменная должна быть инициализирована сразу (тип определяет компилятор). dynamic определяется во время выполнения. Более того, он может быть динамический. Физически dynamic имеет тот  же тип что и System.Object.

Abstract

Ключевое слово abstract довольно интересное. Для себя открыл удобным отмечать методы и свойства ключевым словом abstract. Оно заставляет классы наследники обязательно определить эти методы и свойства. Кроме того, экземпляры абстрактных классов не могут быть созданы (но использовать их как типы можно). 

Implicit и Explicit

Довольно редко используемые ключевые слова. Тем не менее они есть, следует знать о них и в случае необходимости применять. При определении методов преобразования компилятору можно указать какой метод использовать: метод для неявного вызова оператора преобразования или при наличии явного указания типа. Implicit говорит компилятору, что наличие явного типа для преобразования типов не обязательно. Explicit же говорит, что метод может быть вызван только при наличии явного указания типа преобразования.

Например:

Rational a1 = 5; //Неявное приведение Int32 к Rational

Int32 a3 = (Int32) a1; //Явное приведение Rational к Int32

Отличие ref и out

Удивительно, но CLR не делает различий между этими ключевыми словами. Для них будет сгенерирован одинаковый IL код. С другой стороны компилятор C# различает эти слова. И делает это он вот как. 

В случае, если параметр помечен как out, вызывающий код может не инициализировать этот параметр, пока не вызван сам метод. В этом случае метод не может использовать значение параметра, и должен обязательно записать значение прежде чем вернуть управление.

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

Таким образом компилятор C# повышает безопасность кода на этапе компиляции.

Потокобезопасный вызов уведомлений о событиях

Не секрет, что работа с потоками довольно трудная. В отношении с событиями возникает еще большая путаница. Следующий код взят из книги Рихтера (CLR via C#). 

public static class EventArgExtensions
{
    public static void Raise(this TEventArgs e, Object sender, ref EventHandler eventDelegate)
    {
        // Копируется ссылка на поле делегата во временное поле для потокобезопасности
        EventHandler temp = Volatile.Read(ref eventDelegate);

    // Если есть подписчики на это событие, уведомляем их  
    if (temp != null) temp(sender, e);
}

}

Другим вариантом реализации потокобезопасных обработок событий является явное управление регистраций событий. Явная регистрация пишется не в пару строчек кода и требуется для каждого вида событий, поэтому довольно трудная в поддержке. Интересным вопросом в плане понимания работы событий служит следующий пример. Имеется делегат с 5-ю подписчиками на событие. Если в первом обработчике произойдет исключение вызовутся ли следующие обработчики? Как правильно защищаться в таких ситуациях?

Ограничения конструктора в обобщенных методах

Не многие знают, что на типы обобщенных методов можно накладывать ограничения. В таком случае код становится более безопасным и зачастую более удобным в использовании. Наиболее распространенное ограничение на обобщенный тип является указание ссылочного или значимого типа. Для того, чтобы сказать компилятору, что все типы T должны быть ссылочного типа: достаточно указать в определении метода (или класса) следующее ограничение.

public class Foo<T> where T : class

 Все ограничения перечислены в списке ниже.

Интересная особенность, что ограничений может быть несколько.

// 1.

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new() { // … }

// 2.

class Base { } class Test<T, U> where U : struct where T : Base, new() { }

// 3.

public class SampleClass<T, U, V> where T : V { }

Ограничения очень полезны для верификации кода компилятором. Если обобщенный тип рассчитан на использование каких-либо определенных типов, об этом следует указать компилятору и другим разработчикам соответствующими ограничениями.

Комментарии

comments powered by Disqus