Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Два вида констант
Операторы is, as и приведение типов
MyType t = o as MyType; // Fails. o is not MyType
SecondType st = o as SecondType
Подобный материал:
1   ...   11   12   13   14   15   16   17   18   ...   47

Два вида констант1213




Этапа компиляции (const)

Этапа выполнения (readonly)


Желательно отдавать предпочтение константам времени выполнения.


// Compile time constant:

public const int _Millennium = 2000;


// Runtime constant:

public static readonly int _ThisYear = 2004;


Константа этапа компиляции заменяется значением во время компиляции:

if ( myDateTime.Year == _Millennium )

будет заменено на

if ( myDateTime.Year == 2000 )


Константы исполнительного периода вычисляются во время выполнения программы. Команды IL, генерируемые при ссылке на константу «только-для-чтения», используют переменную readonly, а не само значение.


Константы этапа компиляции разрешается использовать с:
  • примитивными типами (целые, вещественные),
  • перечисления (enum),
  • строками.



Константы этапа выполнения являются константами в том смысле, что их нельзя модифицировать после выполнения конструктора.


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

// Does not compile, use readonly instead:

private const DateTime _classCreation = new

DateTime( 2000, 1, 1, 0, 0, 0 );


Константы времени выполнения могут быть любого типа. Их следует инициализировать в конструкторе, но можно использовать и инициализатор. Структуры типа DateTime можно создать с помощью значений readonly; их нельзя создать с помощью const.


Значения readonly можно использовать в случае, когда в каждом экземпляре класса хранятся различные значения. Константы этапа компиляции являются, по определению, статическими константами.


Способ вычисления констант этапа компиляции и времени выполнения влияет на возможность их совместного применения. Предположим, что в сборке Infrastructure определены поля обоих типов: const и readonly.


public class UsefulValues

{

public static readonly int StartValue = 5;


public const int EndValue = 10;

}


В другой сборке делается ссылка на эти значения:

for ( int i = UsefulValues.StartValue; i < UsefulValues.EndValue; i++ )

Console.WriteLine( "value is {0}", i );


Получаем:


Value is 5

Value is 6

...

Value is 9


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


public class UsefulValues

{

public static readonly int StartValue = 105;


public const int EndValue = 120;

}


Можно ожидать, что результат будет такой:


Value is 105

Value is 106

...

Value is 119


Но на самом деле результат вообще не будет получен. Теперь в цикле в качестве начального значения используется 105, а в качестве конечного значения — 10. Компилятор С# вместо ссылки на область памяти, где хранится значение EndValue, помещает в сборку Application значение 10 типа const. Сравните это со значением StartValue. Оно было объявлено как readonly и поэтому будет определено только во время выполнения программы. Следовательно, сборка Application будет использовать новое значение, даже не требуя перекомпиляции; простой инсталляции обновленной версии сборки Infrastructure достаточно, чтобы изменить поведение всех клиентов сборки.


Изменение значения константы типа const должно рассматриваться как изменение интерфейса. Необходимо перекомпилировать все программы, ссылающиеся на эту константу. Изменение значения константы типа readonly является изменением реализации. При его изменении не обязательно перекомпилировать всех клиентов класса.


Решающим преимуществом использования const вместо readonly является производительность: в случае применения известных значений констант может быть сгенерирован более эффективный код, чем при доступе к значениям переменных типа readonly.


Тип const следует применять, если значение должно быть доступно еще на этапе компиляции; это относится к параметрам и перечислениям, а также к тем редким случаям, когда точно известно, что задаваемое значение не будет изменяться от версии к версии. Во всех остальных случаях предпочтительнее использовать константы readonly.


Операторы is, as и приведение типов




С# является строго типизированным языком. Хорошим тоном программирования считается отказ от явного преобразования одного типа в другой во всех случаях, где только возможно. Но иногда приведение типов во время выполнения неизбежно. В С# нередко приходится писать функции, которые принимают параметры System.Object. Вероятно, придется привести тип этих параметров (в нисходящем порядке) к другим типам, будь то классы или интерфейсы. Это можно сделать двумя способами: с помощью оператора as или же с применением старого (еще из языка С) средства — оператора приведения (cast). Кроме того, можно воспользоваться страховочным вариантом: сначала протестировать возможность преобразования с помощью оператора is, а затем использовать as или оператор приведения.


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


При использовании операторов as и is никогда не создается новых экземпляров объектов. Проверяется только соответствие типов.


Рассмотрим пример. Напишем программу, которая должна преобразовать тип произвольного объекта в тип МуТуре. Это можно сделать следующим образом:


object o = Factory.GetObject( );


// Version one:

MyType t = o as MyType;


if ( t != null )

{

// work with t, it's a MyType.

} else

{

// report the failure.

}


Или другой вариант:

object o = Factory.GetObject( );


// Version two:

try {

MyType t;

t = ( MyType ) o;

if ( t != null )

{

// work with T, it's a MyType.

} else

{

// Report a null reference failure.

}

} catch

{

// report the conversion failure.

}


Согласитесь, что первая версия проще и ее легче читать. В ней нет фразы try/catch, следовательно, удается избежать и накладных расходов при выполнении и усилий на программирование. Обратите внимание, что версия с приведением должна кроме перехвата исключительных ситуаций выполнять проверку на null. Если применяется оператор приведения, null может быть преобразован в любой ссылочный тип, a onepaтор as при использовании с пустой ссылкой возвращает null. Так что при работе с операторами приведения следует выполнять проверки на null и перехватывать исключительные ситуации. При использовании as достаточно проверить на null возвращенную ссылку.


Операторы as и cast по-разному трактуют определенные пользователем преобразования типов. Операторы as и is всегда только лишь проверяют тип, которым обладает конвертируемый объект во время выполнения программа; они не производят никаких других операций. Если конкретный объект не относится к целевому типу или если он не является разновидностью целевого типа, эти операторы завершаются отказом. С другой стороны, операторы приведения могут использовать для приведения объекта к запрашиваемому типу с помощью оператора конвертирования типов. Под эту ситуацию попадают и преобразования числовых типов. При приведении длинных (long) типов к коротким (short) может произойти потеря информации.


Те же проблемы остаются незамеченными при приведении определенных пользователем типов. Рассмотрим класс:

public class SecondType

{

private MyType _value;


// …


// Conversion operator.

// This converts a SecondType to a MyType.

public static implicit operator

MyType( SecondType t )

{

return t._value;

}

}


Предположим, что объект типа SecondType создается с помощью Factory.GetObject():

object o = Factory.GetObject( );


// o is a SecondType:

MyType t = o as MyType; // Fails. o is not MyType


if ( t != null )

{

// work with t, it's a MyType.

} else

{

// report the failure.

}


// Другой вариант:

try {

MyType t1;

t = ( MyType ) o; // Fails. o is not MyType

if ( t1 != null )

{

// work with t1, it's a MyType.

} else

{

// Report a null reference failure.

}

} catch

{

// report the conversion failure.

}


Обе версии завершаются отказом. Это происходит из-за того, что компилятор генерирует машинный код, основываясь на типе объекта о, имеющемся на этапе компиляции. На этапе компиляции типом объекта о является тип System.Object. Компилятор замечает, что определенное пользователем преобразование из System. Object в МуТуре отсутствует. Он сравнивает определения объектов System.Object и МуТуре. Если заданных пользователем преобразований не обнаруживается, компилятор генерирует код для проверки типа о во время выполнения программы и выясняет, совпадает ли этот тип с МуТуре. Поскольку этот объект относится к SecondType, все завершается отказом. Компилятор не выполняет проверок, чтобы убедиться, может ли фактический тип объекта о быть преобразован в объект МуТуре во время выполнения программы.


Конвертирование SecondType в МуТуре будет успешным в следующем случае:

object o = Factory.GetObject( );


// Версия номер 3:

SecondType st = o as SecondType;

try {

MyType t;

t = ( MyType ) st;

if ( t != null )

{

// work with T, it's a MyType.

} else

{

// Report a null reference failure.

}

} catch

{

// report the failure.

}


Не следует писать такой уродливый код, но здесь он служит иллюстрацией общей проблемы. Можно использовать параметр System.Object в функции, где выполняются соответствующие преобразования, но при попытке приведении типа возникнет та же проблема:


object o = Factory.GetObject( );


DoStuffWithObject( o );


private void DoStuffWithObject( object o2 )

{

try {

MyType t;

t = ( MyType ) o2; // Fails. o2 is not MyType

if ( t != null )

{

// work with T, it's a MyType.

} else

{

// Report a null reference failure.

}

} catch

{

// report the conversion failure.

}

}


Определенные пользователем операторы преобразований действуют только на тип объекта, доступный во время компиляции объекта, но не на его тип, который он будет иметь во время выполнения. При этом неважно, что существует преобразование во время выполнения между типом о2 и МуТуре. Компилятор не принимает этого во внимание. Поэтому по-разному будет вести себя оператор приведения типа в зависимости от того, каким типом обладает переменная st во время компиляции:


t = ( МуТуре ) st;


Оператор as возвратит тот же самый результат независимо от того, как была объявлена переменная st. Но следует иметь в виду, что если типы переменной st и MyType не связаны наследованием, но в классе st существует оператор преобразования типа, то для следующей строки будет сгенерирована ошибка компиляции:


t = st as МуТуре;


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


object о = Factory.GetValue();


int i = о as int; // He компилируется


Это связано с тем, что тип int относится к числу типов-значений и никогда не может быть пустым (null). Какое значение должно быть сохранено в i, если о не является целым? Значение, оказавшееся в i, может оказаться целым числом, но не типа int. Следовательно, оператор as не может быть использован. Поэтому придется работать с приведением типов:


object o = Factory.GetValue( );

int i = 0;

try {

i = ( int ) o;

} catch

{

i = 0;

}


Однако есть способ избежать возни с операторами приведения. Можно использовать оператор is. При этом отпадет возможность возникновения исключений и необходимость в приведении типов:


object o = Factory.GetValue( );

int i = 0;

if ( o is int )

i = ( int ) o;


Если о имеет тип, который может быть конвертирован в int, например тип double, оператор is возвращает false. Оператор is всегда возвращает false при наличии аргументов null.


Оператор is должен использоваться только в тех случаях, когда нельзя конвертировать тип с помощью as. В других случаях он будет избыточным:


// верно, но избыточно: object о = Factory.GetObjectO; МуТуре t = null; if ( о is МуТуре ) t = о as МуТуре;


To же самое можно написать следующим образом:


// correct, but redundant:

object o = Factory.GetObject( );


MyType t = null;

if ( o is MyType )

t = o as MyType;


Это неэффективно и избыточно. Если вы собираетесь конвертировать тип, используя as, в проверке с помощью is нет необходимости. То, что возвращается из as, достаточно проверить на null.

Предыдущий фрагмент эквивалентен следующему коду:

// correct, but redundant:

object o = Factory.GetObject( );


MyType t = null;

if ( ( o as MyType ) != null )

t = o as MyType;


Вопрос: каким способом осуществляется приведение типов в операторе foreach?


public void UseCollection( IEnumerable theCollection )

{

foreach ( MyType t in theCollection )

t.DoStuff( );

}


В foreach используется оператор приведения типов. Код, генерируемый оператором foreach, эквивалентен такому коду:

public void UseCollection( IEnumerable theCollection )

{

IEnumerator it = theCollection.GetEnumerator( );

while ( it.MoveNext( ) )

{

MyType t = ( MyType ) it.Current;

t.DoStuff( );

}

}

Оператор foreach требует использования оператора приведения типов для поддержки как типов-значений, так и ссылочных типов. За счет этого оператор foreach одинаково ведет себя вне зависимости от того, каким является целевой тип. Однако, поскольку используется оператор приведения, циклы foreach могут генерировать BadCastExceptions.


IEnumerator.Current возвращает System.Object, который естественно не имеет никаких операторов преобразования, поэтому если коллекция состоит из объектов типа SecondType, то по тем же причинам, что и выше, преобразование в MyType не пройдет.

Опять же не будет принято во внимание, что во время выполнения программы такое преобразование возможно.


И, наконец, иногда нужно узнать точный тип объекта, а не просто получить информацию о том, возможно ли преобразование текущего типа к целевому типу. Оператор as возвращает значение true для любого типа, который является разновидностью целевого типа. Метод GetТуре() позволяет определить тип объекта во время выполнения программы. При этом возможна более строгая проверка в сравнении с предлагаемой операторами is и as. GetType() возвращает тип объекта, и его можно сравнить с любым конкретным типом.

Рассмотрим функцию еще раз:


public void UseCollection( IEnumerable theCollection )

{

foreach ( MyType t in theCollection )

t.DoStuff( );

}


Если поместить в коллекцию объекты типа NewType, связанные с MyType как показано ниже, то код из UseCollection будет работать нормально:

public class NewType : MyType

{

// …

}


В итоге можно сказать, что в C# использование операторов is, as предпочтительнее использования оператора приведения типа, применение которого может иметь побочные эффекты.