Проверка типов значений на равенство 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Проверка типов значений на равенство



При проверке на равенство типов значений сохраняется тот же принцип, что и для ссылочных типов: метод ReferenceEquals() используется для сравнения ссылок, Equals() предназначен для сравнения значений, а операция == имеет промежуточное на­значение. Однако существенное отличие заключается в том, что типы значений нуждаются в упаковке для преобразования их в ссылки, чтобы с ними можно было использовать мето­ды. Вдобавок Microsoft предлагает уже перегруженный метод Equals() в классе System.ValueType, предназначенный для проверки равенства типов значений. Когда вызывается sA.Equals(sB), где sA и sB - экземпляры некоторой структуры, то возвращаемое значе­ние будет true или false - в зависимости от того, содержат ли sA и sB одинаковые значе­ния во всех своих полях. С другой стороны, для структур не предусмотрено по умолчанию никакой перегрузки операции ==. Написание (sA == sB) в любом выражении вызовет ошибку компиляции, если только вы не предусмотрите собственной перегрузки этой опе­рации для таких структур.

Другой важный момент, связанный со сравнением типов значений, состоит в том, что ReferenceEquals() всегда для них возвращает false, поскольку для того, чтобы позво­лить вызов метода, типы значений упаковываются в объекты. Даже если вы напишете

bool b=ReferenceEquals(v,v); //v - переменная некоторого типа значений

то все равно получите false, потому что v будет упакована отдельно при преобразовании' ка­ждого параметра, что означает получение разных ссылок. По этой причине незачем вызывать ReferenceEquals() для сравнения двух типов значений, поскольку это не имеет смысла.

Хотя перегрузка Equals() по умолчанию для System.ValueType почти наверняка по­дойдет для большинства определяемых вами структур, можно переопределить ее собствен­норучно специальным образом, чтобы увеличить производительность. К тому же, если тип значений содержит поля типа ссылок, может понадобиться переопределить Equals(), чтобы предоставить для этих полей соответствующую семантику, поскольку перегрузка Equals() по умолчанию просто сравнивает их адреса.

Перегрузка операций

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

Перегрузка операций - понятие, знакомое разработчикам на C++. Однако, поскольку эта концепция является новой для разработчиков на Java и Visual Basic, мы объясним ее здесь. Разработчики на C++, возможно, предпочтут сразу перейти к главному примеру.

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

Matrix а, Ь, с;

// Предположим, что а, b и с уже инициализированы

Matrix d = с * (а + b);

С помощью перегрузки операций компилятору можно сообщить, что делают + и * при, использовании с Matrix, что и позволит записать приведенный выше код. Если вы до сих пор кодировали на языке, который не поддерживал перегрузку операций, то, возможно, ре­шите определить методы, выполняющие такие операции. Результат получится не таким ин­туитивно понятным и, скорее всего, будет выглядеть примерно так:

Matrix d = с.Multiply(a.Add(b));

Как вы знаете, операции вроде + и * изначально предназначены исключительно для работы с предопределенными типами данных и на то имеется важная причина: компиля­тор точно знает, что означают операции общего назначения для этих типов данных. Так, например, ему известно, как сложить два значения long или как разделить одно double на другое double, и он может сгенерировать соответствующий код на промежуточном языке. Однако когда вы определяете собственные классы или структуры, то должны предоставить компилятору необходимые сведения: какие методы доступны для вызова, какие поля со­храняются в каждом экземпляре, и т.п. Аналогично, если вы хотите использовать опера­ции с собственными типами, то должны'сообщить компилятору, что эти операции будут означать в контексте данного класса. Сделать это позволяет перегрузка операций.

Еще одно обстоятельство, которое следует подчеркнуть - это то, что, говоря об опе­рациях, мы имеем в виду не только арифметические операции. Существуют также опера­ции сравнения: ==, <, >,!=, >= и <=. Рассмотрим, к примеру, конструкцию if (a==b). Для классов эта конструкция по умолчанию сравнит две ссылки на предмет того, указывают ли они на одно и то же место в памяти, вместо того, чтобы проверить, содержат ли экзем­пляры объектов одинаковые данные. Для класса string это поведение переопределено таким образом, что сравнение строк на самом деле сравнивает их содержимое. Вы можете сделать нечто подобное с собственными классами. По умолчанию для структур операция == вообще ничего не делает. Попытка сравнить две структуры, чтобы узнать, содержат ли они одинаковые значения, приведет к ошибке компиляции, если только вы не перегрузите операцию ==, чтобы сообщить компилятору, как он должен выполнять такое сравнение.

- Существует множество ситуаций, в которых возможность перегрузки операций позво­ляет генерировать лучше читаемый и интуитивно понятный код.

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

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

- Класс, представляющий денежные суммы (например, в финансовой программе).

- Текстовый процессор или программа анализа текста, использующая классы, кото­рые представляют предложения, высказывания и т.п.; операции могут понадобиться для комбинирования предложений (более сложная версия конкатенации строк).

Однако существует также много типов, для которых перегрузка операций нежелатель­на. Неосмотрительное применение перегрузки операций порождает код, который гораздо труднее понять. Например, умножение двух объектов DateTime даже концептуально не имеет смысла.

Как работают операции

Чтобы понять, как перегружать операции, очень полезно задуматься о том, что проис­ходит, когда компилятор встречает операцию в коде. Используя операцию сложения (+) в качестве примера, предположим, что компилятор обрабатывает следующие строки кода:

int myInteger = 3;

uint myUnsignedInt = 2;

double myDouble = 4.0;

long myLong = myInteger + myUnsignedInt;

double myOtherDouble = myDouble + myInteger;

Что происходит, когда компилятор встречает показанную ниже строку?

long myLong = myInteger + myUnsignedInt;

Компилятор поймет, что ему нужно сложить два целых числа и присвоить результат пе­ременной типа long. Однако выражение mylnteger + myUnsignedInt - это на самом деле интуитивно понятный и удобный синтаксис вызова метода, который складывает вместе два числа. Метод принимает два параметра - myInteger и myUnsignedInt - и возвращает их сумму. Таким образом, компилятор предпринимает те же действия, что и при любом вызове метода, т.е. ищет наиболее подходящую перегрузку операции сложения на основе типов па­раметров; в данном случае — принимающую два целых. Как и с обычными перегруженными методами, желательный тип возврата не влияет на выбор версии метода, который должен вызвать компилятор. В данном примере вызывается перегрузка, которая принимает два па­раметра типа int и возвращает int; затем возвращенное значение преобразуется в long.

Следующая строка заставляет компилятор использовать другую перегрузку операции сложения:

double myOtherDouble = myDouble + myInteger;

В этом экземпляре параметры имеют типы double и int, но так случилось, что не существует перегрузки операции сложения, принимающей такую комбинацию парамет­ров. Вместо этого компилятор идентифицирует в качестве наиболее подходящей версию операции сложения с двумя параметрами типа double и неявно преобразует один int в double. Сложение двух double требует обработки, отличной от сложения двух int. Числа с плавающей точкой сохраняются в виде мантиссы и экспоненты. Их сложение включает битовый сдвиг мантиссы одного из двух параметров double - таким образом, чтобы две экспоненты имели одно и то же значение, сложение мантисс, затем сдвиг мантиссы резуль­тата и настройка его экспоненты для обеспечения максимально возможной точности.

Теперь вы готовы к рассмотрению, что произойдет, если компилятор встретит пример но такой код:

Vector vectl, vect2, vect3;

// инициализировать vectl и vect2

vect3 = vectl + vect2;

vectl = vectl*2;

Здесь Vector - структура, которая определена в следующем подразделе. Компилятор ви­дит, что ему нужно сложить вместе два экземпляра Vector - vectl и vect2. Он ищет пе­регрузку операции сложения, которая принимает в качестве параметров два экземпляра Vector.

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

Пример перегрузки операции: структура Vector

В этом разделе демонстрируется перегрузка операции на примере структуры Vector, представляющей трехмерный математический вектор. Не волнуйтесь, если не очень-то раз­бираетесь в математике - мы постараемся предельно упростить данный пример. Прежде всего, поясним, что трехмерный вектор - это просто три числа (типа double), которые сообщают, насколько далеко перемещается нечто. Переменные, хранящие эти три числа, называются х, у и z: х сообщает, насколько далеко нечто перемещается на восток, у - на се­вер, a z - вверх. Комбинация этих трех чисел дает итоговое перемещение. Например, если х=3.0, у=3.0 и z=l.0 (что обычно записывается как (3.0,3.0,1.0)), это значит, что вы переместились на 3 единицы на восток, 3 - на север и поднялись на 1 единицу вверх.

Вы можете складывать или перемножать векторы с другими векторами или числами. Кстати, в этом контексте мы будем использовать термин скаляр, который на языке мате­матики означает просто число - в терминологии C# это обычный double. Смысл сложе­ния должен быть ясен. Если вы сначала переместитесь по вектору (3.0,3.0,1.0),а затем - по вектору (2.0,-4.0,-4.0), то результирующее перемещение может быть вы­числено простым сложением двух векторов. Сложение векторов означает индивидуальное сложение каждого из их компонентов, поэтому в результате получаем (5.0,-1.0,-3.0). В данном контексте математики пишут c=a+b, где а и b - суммируемые векторы, а с - ре­зультирующий вектор. Хорошо бы меть возможность так же обращаться со структурами Vector.

Тот факт, что в этом примере мы имеем дело со структурами, а не с классами, не важен. Перегрузка операций работает одинаково и с классами, и со структурами.

Ниже приведено определение Vector, содержащее поля-члены, конструкторы, переоп­ределение ToString(), которое позволяет легко просмотреть содержимое Vector и пере­груженную операцию.

namespace Wrox.ProCSharp.OOCSharp

{

struct Vector

{

public double x, y, z;

public Vector(double x, double y, double z)

{

this.x = x;

this.у = у;

this.z = z;

}

public Vector(Vector rhs)

{

x = rhs.x;

у = rhs.y;

z = rhs.z;

}

public override string ToString()

{

return " (" + x + ", " + у + ", " + z + ") ";

}

//...

}

//...

}

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

Конструкторы вроде второго, принимающие единственный аргумент типа Vector, часто называют конструкторами копирования, поскольку они позволяют инициализировать экземпляр класса или структуры простым копированием другого экземпляра. Обратите внимание, что для простоты мы оставляем поля структуры общедоступными (public). Их можно было бы сделать приватными (private) и написать соответствующие свойства для доступа, но это никак не повлияло бы на данный пример, за исключением того, что удли­нило бы код.

Самая интересная часть структуры Vector - перегрузка операции сложения:

public static Vector operator+ (Vector lhs, Vector rhs)

{

Vector result = new Vector(lhs);

result.x += rhs.x;

result.у += rhs.y;

result.z += rhs.z;

return result;

}

Перегрузка операции объявляется почти так же, как метод, за исключением того, что ключевое слово operator сообщает компилятору, что это на самом деле определение пе­регрузки операции. За ключевым словом operator следует символ операции, в данном случае - знак сложения (+). Возвращаемый тип - это тип, который получается после при­менения этой операции. Сложение двух векторов дает в результате вектор, поэтому здесь возвращаемым типом является Vector. В данном конкретном случае перегрузки операции сложения возвращаемый тип совпадает с включающим его классом, но, как будет показано позже, это вовсе не обязательно. Два параметра - это операнды, с которыми операция будет работать. Для бинарных операций (которые принимают два параметра), подобных сложению и вычитанию, первый параметр - это значение, которое записывается слева от знака операции, а второй - значение, записываемое справа.

Следует обратить внимание, что левые параметры принято называть lhs (left-hand side), а правые - rhs (right-handside).

C# требует чтобы перегруженные операции объявлялись как public и static; это зна­чит, что они ассоциированы с классом или структурой, а не с их экземплярами. По этой причине тело перегруженной операции не имеет доступа к нестатическим членам, а также к идентификатору this. И это нормально, потому что параметры предоставляют все вход­ные данные, необходимые операции для решения своей задачи.

Теперь, если вы поняли синтаксис объявления операции сложения, то можете взгля­нуть на то, что происходит внутри нее:

{

Vector result = new Vector (lhs);

result.x += rhs.x;

result.у += rhs.y;

result.z += rhs.z;

return result;

}

Эта часть кода - точно такая же, какая могла бы быть при определении метода, и не­сложно догадаться, что она возвращает вектор, представляющий сумму lhs и rhs. Вы про­сто индивидуально складываете члены х, у и z. Теперь потребуется написать некоторый простой код, чтобы протестировать структуру Vector:

static void Main()

{

Vector vect1, vect2, vect3;

vect1 = new Vector(3.0,3.0,1.0);

vect2 = new Vector(2.0,-4.0,-4.0);

vect3 = vectl + vect2;

Console.WriteLine("vectl = " + vectl.ToString());

Console.WriteLine("vect2 = " + vect2.ToString());

Console.WriteLine("vect3 = " + vect3.ToString());

}

Сохраним этот код в файле Vectors.cs, скомпилируем и после его запуска получим:

vect1 = (3,3,1)

vect2 = (2,-4,-4)

vect3 = (5,-1,-3)



Поделиться:


Последнее изменение этой страницы: 2016-12-30; просмотров: 214; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.144.248.24 (0.039 с.)