ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
carArray[2] = new Car("Zippy, 30, 0);
carArray[3] = new Car("Fjred", 30, 0);}
public IEnumerator GetEnumerator() {
// Возвращает IEnumerator объекта массива.
return carArray.GetEnumerator();
}
}
Теперь, после модификации типа Garage, вы можете использовать этот тип в конструкции foreach без опасений. К тому же, поскольку метод GetEnumerator() определен, как открытый, пользователь объекта тоже может взаимодействовать с типом IEnumerator.
// Manually work with IEnumerator.
IEnumerator I = carLot.GetEnumerator();
i.MoveNext();
Car myCar = (Car)i.Current;
Console.WriteLine("{0} имеет скорость {1} км/ч", myCar.PetName, myCar.CurrSpeed);
Если вы предпочтете скрыть функциональные возможности IEnumerable на объектном уровне, то следует использовать явную реализацию интерфейса.
public IEnumerator IEnumerable.GetEnumerator() {
// Возвращает IEnumerator объекта массива.
return carArray.GetEnumerator();
}
Исходный код. Проект CustomEnumerator размещен в подкаталоге, соответствующем главе 7.
Методы итератора в C#
В .NET 1.x для того, чтобы пользовательские коллекции (такие, как Garage) допускали применение конструкции foreach в операциях, подобных перечислению, реализация интерфейса IEnumerable (и, как правило, интерфейса IEnumerator) была обязательной. В C# 2005 предлагается альтернативный вариант построения типов, позволяющих применение цикла foreach, – с помощью итераторов.
В упрощённой интерпретации итератор является членом, указывающим порядок возвращения внутренних элементов контейнера при их обработке с помощью foreach. И хотя метод итератора все равно должен называться GetEnumerator(), а возвращаемое значение все равно должно иметь тип IEnumerator, при таком подходе ваш пользовательский класс уже не обязан реализовывать все ожидаемые интерфейсы.
public class Garage { // Без реализации IEnumerator!
private Car[] carArray;
…
// Метод итератора.
public IEnumerator GetEnumerator() {
foreach (Car с in carArray) {
yield return c;
}
}
}
Обратите внимание на то, что данная реализация GetEnumerator() осуществляет "проход" по вложенным элементам, используя внутреннюю логику foreach, и возвращает объекты Car вызывающей стороне, используя новую синтаксическую конструкцию yield return. Ключевое слово yield используется для того, чтобы указать значение (или значения), возвращаемые конструкции foreach вызывающей стороны. Когда в программе встречается оператор yield return, сохраняется текущая позиция, и именно с этой позиции выполнение будет продолжено при следующем вызове итератора.
Когда компилятор C# обнаруживает метод итератора, в рамках области видимости соответствующего типа (в данном случае это Garage) динамически генерируется вложенный класс. Этот автоматически сгенерированный класс реализует интерфейсы IEnumerable и IEnumerator и указывает необходимые параметры членов GetEnumerator(), MoveNext(), Reset() и Current. Если теперь загрузить данное приложение в ildasm.exe, то будет видно, что внутренняя реализация GetEnumerator() в объекте Garage использует сгенерированный компилятором тип (который в данном примере получает имя ‹GetEnumerator›d__0).
.method public hidebysig instance class [mscorlib] System.Collections.IEnumerator GetEnumerator() cil managed {
…
newobj instance void CustomEnumeratorWithYield.Garage/ '‹GetEnumerator›d__0'::.ctor(int32)
…
} // end of method Garage::GetEnumerator
Явно, что от предложенного здесь определения метода итератора мы не получим большой пользы, поскольку наш тип Garage изначально реализовывал GetEnumerator(), ссылаясь на внутренний тип System.Array. Но синтаксис итератора C# может сэкономить немало времени при построении более "экзотических" пользовательских контейнеров (например, бинарных деревьев), где приходится вручную реализовать интерфейсы IEnumerator и IEnumerable. В любом случае программный код вызывающей стороны при взаимодействии с методом итератора с использованием foreach оказывается одинаковым.
static void Main(string[] args) {
Console.WriteLine("***** Забавы с методами итератора *****n");
Garage carLot = new Garage();
foreach (Car с in carLot) {
Console.WriteLine("{0} имеет скорость {1} км/ч", с.PetName, с.CurrrSpeed);
}
Console.ReadLine();
}
Исходный код. Проект CustomEnumeratorWifhYield размещен в подкаталоге, соответствующем главе 7.
Создание клонируемых объектов (ICloneable)
Вы, должно быть, помните из главы 3, что System.Object определяет член с именем MemberwiseClone(). Указанный метод используется для получения поверхностной копии объекта. Пользователи объекта не могут вызвать этот метод непосредственно (поскольку он является защищенным), но сам объект может вызвать этот метод в процессе клонирования. Для примера предположим, что у нас есть класс с именем Point (точка).
// Класс Point.
public class Point {
// Открыты для простоты.
public int x, у;
public Point(int x, int y) { this.x = x; this.у = у; }
public Point(){}
// Переопределение Object.ToString().
public override string ToString() { return string.Format("X = {0}; Y = {1}", x, у); }
}
С учетом того, что вы уже знаете о ссылочных типах и типах, характеризуемых значениями (см. главу 3), вы должны понимать, что в результате присваивания одной ссылочной переменной другой получаются две ссылки, указывающие на один и тот же объект в памяти. Поэтому следующее присваивание дает две ссылки на один и тот же объект Point в динамической памяти, и модификации любой из этих ссылок будут влиять на этот объект.
static void Main(string[] args) {
// Две ссылки на один и тот же объект!
Point p1 = new Point(50, 50);
Point p2 = p1;
р2.х = 0;
Console.WriteLine(p1);
Console.WriteLine(p2);
}
Чтобы обеспечить пользовательскому типу возможность возвращать копию этого типа вызывающей стороне, можно реализовать стандартный интерфейс ICloneable. Этот интерфейс определяет единственный метод с именем Clone().
public interface ICloneable {
object Clone();
}
Очевидно, что реализация метода Clone() будет зависеть от объекта. Но базовые функциональные возможности оказываются одинаковыми: это копирование значений членов-переменных в новый экземпляр объекта и возвращение этого экземпляра пользователю. В качестве иллюстрации рассмотрите следующую модификацию класса Point.
// Теперь Point поддерживает клонирование.
public class Point: ICloneable {
public int x, y;
public Point(){}
public Point (int x, int y) { this.x = x; this.у = у; }
// Возвращение копии данного объекта.
public object Clone() { return new Point(this.x, this.y); }
public override string ToString() { return String.Format("X = {0}; Y = {1}", x, у); }
}
С помощью указанного подхода можно создавать точные и независимые копии типа Point, как показано в следующем фрагменте программного кода.
static void Main (string[] args) {
// Обратите внимание, Clone() возвращает объект общего типа.
// Для получении производного типа используйте явное преобразование.
Point р3 = new Point(100, 100);
Point р4 = (Point)р3.Clone();
// Изменение p4.х (это не изменит р3.х).
р4.х = 0;
// Вывод объектов.
Console.WriteLine(р3);
Console.WriteLine(p4);
}
Текущая реализация Point решает все поставленные задачи, но вы можете немного усовершенствовать процесс. Введу того, что тип Point не содержит переменных ссылочного типа, можно упростить реализацию метода Clone(), как показано ниже.
public object Clone() {
// Скопировать все поля Point "почленно".
return this.MemberwiseClone();
}
При наличии в Point членов-переменных ссылочного типа метод MemberwiseClone() скопирует ссылки на соответствующие объекты (т.е. выполнит поверхностное копирование). Чтобы обеспечить поддержку полного копирования объектов, вы должны в процессе клонирования создать новый экземпляр для каждой переменной ссылочного типа. Соответствующий пример мы сейчас рассмотрим.
Пример клонирования
Предположим, что класс Point содержит член ссылочного типа с именем PointDescription, обеспечивающий поддержку "понятного" имени объекта Point и его идентификационного номера в виде System.Guid (еcли у вас нет опыта применения COM, знайте, что GUID – глобально уникальный идентификатор – это статистически уникальное 128-разрядное значение). Вот соответствующая реализация.
// Этот класс описывает точку.