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

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

Содержание


0 в типах-значениях
Методы ReferenceEquals(), Equals(), статический метод Equals() и operator==
Подобный материал:
1   ...   15   16   17   18   19   20   21   22   ...   47

0 в типах-значениях15


По умолчанию при инициализации системой .NET значения всех

объектов устанавливаются в 0. Особый случай составляют

перечисления (enum). Никогда не создавайте перечисления, в

которые 0 не включен как допустимое значение. Все перечисления

наследуют от System.ValueType. Значения перечислений

начинаются с нуля, но это поведение может быть изменено:

public enum Planet

{

// Explicitly assign values.

// Default starts at 0 otherwise.

Mercury = 1,

Venus = 2,

Earth = 3,

Mars = 4,

Jupiter = 5,

Saturn = 6,

Neptune = 7,

Uranus = 8,

Pluto = 9

}

Planet sphere = new Planet();

Переменная sphere имеет значение 0, что не является допустимым

значением. При создании собственных значений для перечислений

убедитесь в том, что одним из таких значений является 0.

Для этого примера придется в обязательном порядке явно

инициализировать значение:

Planet sphere = Planet.Mars;


Труднее, однако, ввести переменную типа Planet в новый тип:

public struct ObservationData

{

Planet _whichPlanet; //what am I looking at?

Double _magnitude; // perceived brightness.

}

Вновь созданное ObservationData имеет значение _magnitude равное

0. Но значение переменной _whichPlanet является недопустимым.

У перечисления Planet отсутствует очевидное значение по

умолчанию. Следует сделать так:

public enum Planet

{

None = 0,

Mercury = 1,

Venus = 2,

Earth = 3,

Mars = 4,

Jupiter = 5,

Saturn = 6,

Neptune = 7,

Uranus = 8,

Pluto = 9

}

Planet sphere = new Planet();


Теперь sphere содержит значение None. Добавление этого

неинициализированного значения в перечисление Planet вносит

изменения в структуру ObservationData. Вновь созданные объекты

ObservationData имеют значение _magnitude равное 0, а в

_whichPlanet — None. Добавьте явный конструктор, чтобы

позволить инициализировать все поля явно:

public struct ObservationData

{

Planet _whichPlanet; //what am I looking at?

Double _magnitude; // perceived brightness.

ObservationData( Planet target, Double mag )

{

_whichPlanet = target;

_magnitude = mag;

}

}

Но помните, что конструктор по умолчанию по-прежнему является

доступным. Клиенты класса по-прежнему могут создавать

инициализируемый по умолчанию вариант.


Методы ReferenceEquals(), Equals(), статический метод Equals() и operator==




При создании собственных типов (будь то классы (class) или структуры (struct)), необходимо определить, что означает для них равенство. В С# предлагаются четыре функции, с помощью которых можно узнать, являются ли два различных объекта «равными»:

public static bool ReferenceEquals

( object left, object right );

public static bool Equals

( object left, object right );

public virtual bool Equals( object right);

public static bool operator==( MyClass left, MyClass right );

Язык позволяет создавать собственные версии всех этих четырех методов. Но наличие такой возможности вовсе не означает, что это следует делать. Никогда не переопределяйте первые две статические функции. Скорее всего, понадобится переопределить обычный метод Equals(). Кроме того, от случая к случаю можно переопределять operator==, но только по соображениям производительности и только для типов-значений. Более того, между этими четырьмя функциями имеются взаимоотношения, так что, изменив одну из них, можно повлиять на поведение остальных. Да, необходимость применения для проверки равенства четырех функций является очевидным усложнением. Но эту ситуацию можно упростить.


Как и в случаях с другими элементами С#, это усложнение является следствием того, что С# позволяет создавать типы-значения и ссылочные типы. Две переменные ссылочного типа считаются равными, если они ссылаются на один и тот же объект, который принято называть «личностью объекта» (object identity). Две переменные, принадлежащие типу-значению, считаются равными, если они имеют один тип и одинаковый контент. Вот почему для проверки на равенство необходимо иметь столько разных методов.


Начнем с тех двух функций, которые никогда не следует заменять. Функция Object.ReferenceEquals() возвращает true, если две сравниваемые переменные относятся к одному и тому же объекту, т.е. если у этих двух переменных совпадают личности объектов. Независимо от того, являются ли сравниваемые объекты ссылочными типами или типами-значениями, проверке всегда подвергается личность объекта, а не его содержимое (контент). Это означает, что при проверке на равенство типов-значений ReferenceEquals() всегда возвращает false. Даже если сравнивать тип-значение с самим собой, все равно функция ReferenceEquals() возвратит false. Это происходит из-за упаковки (boxing).

int i = 5;

int j = 5;

if ( Object.ReferenceEquals( i, j ))

Console.WriteLine( "Never happens." );

else

Console.WriteLine( "Always happens." );


if ( Object.ReferenceEquals( i, i ))

Console.WriteLine( "Never happens." );

else

Console.WriteLine( "Always happens." );


Переопределять Object.ReferencesEquals() не требуется, потому что эта функция делает именно то, что от нее и ожидается: сравнивает личности объектов для двух различных переменных.


Второй функцией, переопределять которую не следует никогда, является статическая функция Object.Equals(). Она проверяет, являются ли равными две переменные, если неизвестно, каким будет фактический тип аргументов во время выполнения. Напомним, что System.Object является основным базовым классом для абсолютно всего в С#. Всякий раз, когда происходит сравнение двух переменных, они представляют собой экземпляры класса System.Object. Типы-значения и ссылочные типы также являются экземплярами System.Object. Так как же может этот метод проверять равенство двух переменных, не зная при этом, к какому конкретно типу они принадлежат, если само понятие равенства различно для разных типов? Ответ прост: этот метод делегирует свои полномочия одному из типов, о которых идет речь. В статическом методе Object.Equals() реализуется что-то вроде:

public static bool Equals( object left, object right )

{

// Check object identity

if (left == right )

return true;

// both null references handled above

if ((left == null) || (right == null))

return false;

return left.Equals (right);

}


В этом примере используются метод operator==() и метод Equals() для экземпляров. Мы рассмотрим их позже, а сейчас лишь отметим, что статический метод Equals() применяет метод Equals() для экземпляра левого аргумента, чтобы определить, являются ли два объекта равными.


Как и в случае с RefеrenceEquals(), никогда не следует переопределять статический метод Object.Equals(), поскольку он делает именно то, чего от него ожидают: определяет, являются ли два объекта одинаковыми, если нам неизвестно, какой тип они будут иметь во время выполнения. Поскольку статический метод Equals() делегирует свои полномочия методу Equals() для экземпляра левого объекта, он использует правила типа для этого объекта.


Итак, никогда не следует переопределять статические методы ReferenceEquals() и Equals().


Прежде чем перейти к обсуждению методов, которые можно переопределять, вспомним математические свойства отношения равенства. Необходимо, чтобы ваше определение и ваша реализация согласовывались с общепринятыми правилами. Поэтому нужно учитывать математические свойства равенства: равенство является рефлексивным, симметричным и транзитивным. Свойство рефлексивности означает, что любой объект равен самому себе. Независимо от того, какой тип используется, а == а всегда истинно. Свойство симметрии означает, что порядок объектов не играет роли: если справедливо утверждение а == b, то будет справедливо и утверждение b == а. Если же утверждение а == b ложно, то ложным является и утверждение b == а. В последнем свойстве говорится о том, что если верны оба утверждения а == b и b == с, то верно и утверждение а == с. Это свойство называется свойством транзитивности.


Теперь рассмотрим функцию Object.Equals() для экземпляров. Собственную версию Equals() для экземпляров следует создавать только в том случае, когда поведение по умолчанию не согласуется с создаваемым типом. Метод Object.Equals() использует личность объекта для определения того, равны ли две сравниваемые переменные. Применяемая по умолчанию функция Object.Equals() ведет себя точно так же, как и Object.ReferenceEquals(). Но как же так? Ведь типы-значения отличаются от ссылочных типов! В System.ValueType переопределяется метод Object.Equals(). Напомним, что ValueТуре является базовым типом для всех создаваемых (с использованием слова struct) типов-значений. Две переменные, принадлежащие типу-значению, равны в том случае, если они относятся к одному типу и имеют одинаковое содержимое. ValueType.Equals() реализует именно такое поведение. К сожалению, для ValueType.Equals() отсутствует хорошая, эффективная реализация. Чтобы получить правильный результат, она должна уметь сравнивать все элементы экземпляра класса для любого производного от ValueType типа, не требуя знаний того, каким именно будет тип производного объекта во время выполнения программы. В С# это означает использование отражения (reflection), о котором мы поговорим далее. Как будет показано, у отражения имеется много недостатков, особенно в тех случаях, когда важна производительность. Проверка на равенство является одной из тех фундаментальных конструкций, которые очень часто вызываются в программах, так что производительность здесь действительно важна. Практически всегда можно написать намного более быстрый вариант Equals() для нужного типа-значения. Рекомендации для типов-значений просты: всякий раз при создании типа-значения следует создавать переопределение ValueType. Equals().


Переопределять функцию Equals() для экземпляров следует только в случае, когда необходимо изменить определенную для ссылочного типа семантику. В некоторых случаях классы библиотеки классов (Class Library) .NET Framework используют для проверок на равенство семантику значений вместо семантики ссылок. Два строковых объекта считаются равными, если они содержат одинаковый контент. Для создаваемых вами типов правило заключается в том, что если тип должен следовать семантике значений (сравнивать содержимое), а не семантике ссылок (сравнивать личности объектов), необходимо написать собственное переопределение Object.Equals().


Теперь мы знаем, когда следует писать собственное переопределение Object.Equals(). Посмотрим, как его можно реализовать. Отношение равенства для типов-значений сильно влияет на упаковку (boxing). Чтобы не возникало странных сюрпризов для пользователей класса, в случае ссылочных типов метод экземпляра должен вести себя ожидаемо. Ниже приводится стандартная модель:

public class Foo

{

public override bool Equals( object right )

{

// check null:

// this pointer is never null in C# methods.

if (right == null)

return false;


if (object.ReferenceEquals( this, right ))

return true;


if (this.GetType() != right.GetType())

return false;


// Compare this type's contents here:

return CompareFooMembers(

this, right as Foo );

}

}

Во-первых, при работе Equals() никогда не возникает исключительных ситуаций — в этом нет никакого смысла. Две переменные либо равны, либо не равны; здесь нет места для каких-либо других вариантов. Нетрудно возвратить значение false во всех ошибочных ситуациях, например при наличии null-ссылок или неверных типов аргументов. Теперь углубимся в детали этого метода, чтобы понять, что именно делает каждая из проверок и почему от некоторых проверок можно отказаться. Первая проверка определяет, не является ли пустым объект, расположенный с правой стороны. Для ссылки this подобная проверка не нужна. В С# объект this не может быть null. CLR возбудит исключение еще перед вызовом любого метода для экземпляра с null-ссылкой. Следующая проверка выясняет, являются ли эти два объекта одинаковыми, сравнивая личности объектов. Это очень эффективная проверка, и равенство личностей объектов служит гарантией равенства их содержимого.


Далее проверяется, относятся ли два сравниваемых объекта к одному типу. Здесь важно именно точное сравнение типов переменных во время выполнения программы. Во-первых, обратите внимание, что в методе не делается предположения о том, что объект this относится к типу Foo; метод вызывает this.GetТуре(). Фактический тип переменной this может быть классом, производным от Foo. Во-вторых, программа выясняет точный тип подлежащих сравнению объектов. Недостаточно убедиться в том, что стоящий справа параметр может быть конвертирован в текущий тип. При проверке могут возникнуть две незаметные на первый взгляд ошибки. Рассмотрим следующий пример, в котором применяется наследование:

public class B

{

public override bool Equals( object right )

{

// check null:

if (right == null)

return false;


// Check reference equality:

if (object.ReferenceEquals( this, right ))

return true;


// Проблема в этом месте, обсудим ее далее.

B rightAsB = right as B;

if (rightAsB == null)

return false;


return CompareBMembers( this, rightAsB );

}

}


public class D : B

{

// etc.

public override bool Equals( object right )

{

// check null:

if (right == null)

return false;


if (object.ReferenceEquals( this, right ))

return true;


// Проблемы будут при использовании обоих вариантов.

// 1ый вариант (в случае right типа B вернет null и в целом false)

D rightAsD = right as D;

if (rightAsD == null)

return false;


// 2ой вариант (даже, если вместо rightAsD будет right типа B, будут сравнена только базовая часть от D и right; если же оставить rightAsD, то в метод базового класса будет передан null)

if (base.Equals(rightAsD) == false)

return false;


return CompareDMembers( this, rightAsD );

}


}


//Test:

B baseObject = new B();

D derivedObject = new D();


// Comparison 1.

if (baseObject.Equals(derivedObject))

Console.WriteLine( "Equals" );

else

Console.WriteLine( "Not Equal" );


// Comparison 2.

if (derivedObject.Equals(baseObject))

Console.WriteLine( "Equals" );

else

Console.WriteLine( "Not Equal" );


Что должна вывести такая программа? Второе сравнение никогда не возвращает значение true. baseObject типа В не может быть преобразован в тип D. Однако первое сравнение может дать значение true. derivedObject типа D может быть неявно конвертирован в тип В вверх по иерархии наследования. Если поля класса В стоящего справа аргумента соответствуют полям класса В аргумента, стоящего слева, В. Equals() считает, что объекты равны. Мы разрушили свойство симметричности функции Equals. Эта конструкция разрушается из-за автоматических преобразований типов вверх и вниз по иерархии наследования.


Если написать следующий фрагмент, объект D будет явно конвертироваться в тип В:


baseObject.Equals(derivedObject)


Если baseObject.Equals() выяснит, что поля, определенные в типе, совпадают, то два объекта будут считаться равными. С другой стороны, при такой записи будет невозможным преобразование объекта В в объект D:


derivedObject.Equals(baseObject)


Объект В не может быть конвертирован в объект D. Метод derivedObject.Equals() всегда будет возвращать false. Если не выполнить точную проверку типов объектов, можно оказаться в ситуации, когда играет роль порядок сравнения (B = D, но D != B).


Встречается и другой подход к переопределению Equals(). Этот метод стоит вызывать из базового класса только в том случае, если он не перенаправляет вызов в System.Object или System.ValueType. Эта проблема показана в предыдущем примере. Класс D вызывает метод Equals(), определенный в базовом классе — классе В. Однако, если класс В не предоставит своей реализации, а вызовет версию, определенную в System.Object, то это совсем не то, что нужно. Версия из System.Object возвращает значение true только в том случае, когда оба аргумента ссылаются на один и тот же объект. В коде, который приведен выше при вызове base.Equals( right ) происходит сравнение базовых частей текущего объекта и объекта переданного как аргумент.


Правило таково: следует переопределять Equals() всякий раз, когда создается тип-значение, и нужно переопределять Equals() для ссылочных типов, если нежелательно, чтобы ссылочный тип следовал ссылочной семантике, задаваемой System.Object.


И наконец, рассмотрим четвертый метод: operator==(). Всякий раз, когда приходится иметь дело с типом-значением, следует переопределить operator==(). Причина здесь та же самая, что и в случае функции Equals() для экземпляров. Версия, используемая по умолчанию, применяет отражение для сравнения содержимого двух типов-значений. Это менее эффективно, чем любая написанная вами реализация, так что смело пишите свою собственную.


Если создаются ссылочные типы, переопределять operator==() приходится редко. Если вы передаете свой объект, который является экземпляром ссылочного типа, в объекты класса, определенные в .NET Framework, то последние ожидают, что operator==() будет следовать ссылочной семантике.