C# для профессионалов. Том II - Симон Робинсон
Шрифт:
Интервал:
Закладка:
□ Операторы побитового присваивания &=, | =, ^=, >>= и <<=.
□ Булевы операторы &&, ||. (Они определяются компилятором из соответствующих побитовых операторов.)
□ Оператор присваивания =. Значение этого оператора в C# фиксировано.
Существует также ограничение в том, что операторы сравнения должны перезагружаться парами, другими словами, при перезагрузке == необходимо перезагрузить также != и наоборот. Аналогично, если перезагружается один из операторов < и <=, то необходимо перезагрузить оба оператора и так же для > и >=. Причина этого состоит в необходимости обеспечения согласованной поддержки для любых типов данных базы данных, которые могут иметь значение null и для которых поэтому, например, == не обязательно имеет результат, противоположный !=.
После определения того, что оператор, который требуется перезагрузить, является таким, который можно перезагрузить в C#, синтаксис для реального определения перезагруженной версии значительно проще, чем соответствующий синтаксис в C++. Единственный момент, о котором необходимо помнить при перегрузке операторов C#, состоит в том, что они всегда должны объявляться как статические члены класса. Это противоположно ситуации в C++, где можно определить свои операторы либо как статические члены класса, как член экземпляра класса (но беря на один параметр меньше), либо как функцию, которая не является членом класса вообще.
Причина того, что определение перезагруженных версий операторов настолько проще в C#, не имеет на самом деле ничего общего с самими перезагруженными версиями операторов. Это связано со способом, которым осуществляется управление памятью в C#. Определение перезагруженных версий операторов в C++ является областью, которая заполнена ловушками, Рассмотрим, например, попытку перезагрузить оператор сложения для класса в C++. (Предполагается для этого, что CMyClass имеет член x и сложение экземпляров означает сложение членов x.). Код может выглядеть следующим образом (предполагается, что перезагруженная версия является явной вставкой кода):
static CMyClass operator+(const CMyClass &lhs, const CMyClass &rhs) {
CMyClass Result;
Result.x = lhs.x + rhs.x;
return Result;
}
Отметим, что оба параметра объявлены как const и передаются по ссылке, чтобы обеспечить оптимальную производительность. Это само по себе не слишком плохо. Однако теперь для возвращения результата, необходимо создать временный экземпляр CMyClass внутри перезагруженной версии оператора. Конечная инструкция return Result выглядит безопасной, но она будет компилироваться только в том случае, если доступен оператор присваивания для копирования Result из функции.
Это само по себе является нетривиальной задачей, так как если ссылки используются неправильно при определении, то очень легко случайно определить ссылку, которая рекурсивно вызывает себя, пока не будет получено переполнение стека. Перезагрузка операторов в C++ является задачей для опытных программистов. Нетрудно видеть, почему компания Microsoft решила сделать некоторые операторы неперезагружаемыми в C#.
В C# практика будет другой. Здесь нет необходимости явно передавать по ссылке, так как классы C# являются ссылочными переменными в любом случае (а для структур передача по ссылке снижает производительность). И возвращение значения является легкой задачей. Будет ли это класс или структура, надо просто вернуть значение временного результата, а компилятор C# гарантирует, что в результате будут скопированы либо поля-члены (для типов данных значений), либо адреса (для ссылочных типов). Единственный недостаток заключается в том, что нельзя использовать ключевое слово const, чтобы получить дополнительную проверку компилятора, которая определяет, изменяет или нет перезагруженная версия оператора параметры класса. Также C# не предоставляет улучшения производительности подставляемых функций, как происходит в C++.
static MyClass operator+(MyClass lhs, CMyClass rhs) {
MyClassResult = new MyClass();
Result.x = lhs.x + rhs.x;
return Result;
}
Индексаторы
C# cтрого не разрешает перезагружать []. Однако он позволяет определить так называемые индексаторы (indexer) класса, что обеспечивает такой же результат.
Синтаксис определении индексатора очень похож на синтаксис свойства. Предположим, что необходимо использовать экземпляры MyClass как массив, где каждый элемент индексируется с помощью int и возвращает long. Тогда можно сделать следующую запись:
class MyClass {
public long this[int x] {
get {
// код для получения элемента
}
set {
// код для задания элемента, например X = value;
}
}
// и т.д.
Код внутри блока get выполняется всякий раз, когда Mine[x] стоит с правой стороны выражения (при условии, что выражение Mine является экземпляром MyClass и x будет int), в то время как блок set выполняется только тогда, когда Mine[x] указывается с левой стороны выражения. Блок set ничего не может вернуть и использует ключевое слово value для указания величины, которая появится с правой стороны выражения. Блок get должен вернуть тот же тип данных, который имеет индексатор.
Можно перезагружать индексаторы, чтобы использовать любой тип данных в квадратных скобках или любое число аргументов, тем самым создавая эффект многомерного массива.
Определенные пользователем преобразования типов данных
Так же как для индексаторов и [], C# формально не рассматривает () как оператор, который может перезагружаться, однако C# допускает определяемые пользователем преобразования типов данных, которые имеют тот же результат. Например, предположим, что имеются два класса (или структуры) с именами MySource и MyDest и необходимо определить преобразование типа из MySource в MyDest. Синтаксис этого выглядит следующим образом:
public static implicite operator MyDest(MySource Source) {
// код для выполнения преобразования. Должен возвращать экземпляр MyDest
}
Преобразование типа данных определяется как статический член класса MyDest или класса MySource. Оно должно также объявляться любо как implicit, либо как explicit. Если преобразование объявлено как implicit, то оно используется неявно:
MySource Source = new MySource();
MyDest Dest = MySource;
Если преобразование объявлено как explicit, то оно может использоваться только явным образом:
MySource Source = new MySource();
MyDest Dest = (MyDest)MySource;
Необходимо определять неявные преобразования типов данных в том случае, когда они всегда работают, а явные преобразования типов только тогда, когда может произойти отказ в связи с потерей данных или порождением исключения.
Так же как и в C++, если компилятор C# встречается с запросом преобразования между типами данных, для которых не существует прямого преобразования типов, он будет стараться найти "лучший" способ, используя доступные методы преобразования типов. Существуют те же вопросы, что и в C++, в отношении интуитивной ясности преобразований типов данных, а также в том, что различные пути получения преобразования не создают несовместимых результатов.
C# не позволяет определить преобразования типов данных между классами, которые являются производными друг друга. Такие преобразования уже доступны — неявно из производного класса в базовый класс и явно из базового класса в производный.
Отметим, что если попробовать выполнить преобразование ссылки базового класса в ссылку производного класса, и при этом рассматриваемый объект не является экземпляром производного класса (или какого-нибудь производного из него), то будет порождаться (генерироваться) исключение. В C++ нетрудно преобразовать указатель на объект в "неправильный" класс объектов. Это просто невозможно в C# с помощью ссылок. По этой причине преобразование типов в C# считается более безопасным, чем в C++.
// пусть MyDerivedClass получен из MyBaseClass
MyBaseClass MyBase = new MyBaseClass();
MyDerivedClass MyDerived = (MyDerivedClass) MyBase; // это приведет
// к порождению исключения
Если нежелательно преобразовывать что-то в производный класс, но нежелательно также, чтобы генерировалось исключение, можно применить ключевое слово as. При использовании as, если преобразование отказывает, будет возвращаться null.
// пусть MyDerivedClass получен из MyBaseClass