С помощью оператора yieldможно также делать и более сложные вещи, например, возвращать перечислитель из yield return.
В игре “крестики-нолики” имеется девять полей, где игроки-соперники расставляют крестики или нолики. Эти ходы имитируются классом GameMoves. Метод Cross() и Circle() - это блоки итератора для создания типов итераторов. Переменные crossи circleустанавливаются в Cross() и Circle() внутри конструктора класса GameMoves. Для установки этих полей методы не вызываются, но устанавливаются в типы итераторов, определенные в блоках итераторов. Внутри блока итератора Cross() информация о ходах выводится на консоль и номер хода увеличивается. Если номер хода больше 8, итератор завершается с помощью yieldbreak; в противном случае на каждой итерации возвращается объект перечислителя circle. Блок итератора Circle() очень похож на блок итератора Cross(), только он возвращает на каждой итерации объект перечислителя cross.
public class GameMoves
{
private IEnumerator cross;
private IEnumerator circle;
public GameMoves()
{
cross = Cross();
circle = Circle();
}
private int move = 0;
const int MaxMoves = 9;
public IEnumerator Cross()
{
while (true)
{
Console.WriteLine("Крестик, ход {0}", move);
if (++move >= MaxMoves) yield break;
yield return circle;
}
}
public IEnumerator Circle()
{
while (true)
{
Console.WriteLine("Нолик, ход {0}”, move);
if(++move >= MaxMoves) yield break;
yield return cross;
}
}
}
В клиентской программе использовать класс GameMovesможно так, как показано ниже. Первый ход выполняется установкой перечислителя в тип перечислителя, возвращенный game.Cross(). В цикле whileвызывается enumerator.MoveNext. При первом его вызове вызывается метод Cross(), возвращающий другой перечислитель с помощью оператора yield. Возвращенное значение можно получить через свойство Currentи затем оно устанавливается в переменную enumeratorдля следующего шага цикла:
GameMoves game = new GameMoves();
IEnumerator enumerator = game.Cross();
while (enumerator.MoveNext())
{
enumerator = enumerator.Current as IEnumerator;
}
Вывод этой программы показывает все ходы игроков до самого последнего:
Крестик, ход 0
Нолик, ход 1
Крестик, ход 2
Нолик, ход 3
Крестик, ход 4
Нолик, ход 5
Крестик, ход 6
Нолик, ход 7
Крестик, ход 8
Кортежи
Массивы комбинируют объекты одного типа, а кортежи (tuple) могут комбинировать объекты различных типов. Понятие кортежей происходит из языков функционального программирования, таких как F#, где они часто используются. С появлением.NET 4 кортежи стали доступны в.NET Framework для всех языков.NET.
В.NET 4 определены восемь обобщенных классов Tupleи один статический класс Tuple, который служит фабрикой кортежей. Существуют различные обобщенные классы Tupleдля поддержки различного количества элементов; например, Tuple<T1>содержит один элемент, Tuple<T1,Т2>- два элемента и т.д.
Метод Divide() демонстрирует возврат кортежа с двумя членами - Tuple<int,int>. Параметры обобщенного класса определяют типы членов, которые в данном случае оба целочисленные. Кортеж создан статическим методом Create() статического класса Tuple. Обобщенные параметры метода Create() определяют тип создаваемого экземпляра кортежа. Вновь созданный кортеж инициализируется переменными resultи reminderдля возврата результата деления:
public static Tuple<int,int> Divide (int dividend, int divisor)
{
int result = dividend/divisor;
int reminder = dividend%divisor;
return Tuple.Create<int, int>(result, reminder);
}
В следующем коде показан вызов метода Divide(). Элементы кортежа могут быть доступны через свойства Item1и Item2.
var result = Divide(5,2);
Console.WriteLine("результат деления: (0), остаток: {1}",
result.Item1, result.Item2);
В случае если имеется более восьми элементов, которые нужно включить в кортеж, можно использовать определение класса Tupleс восемью параметрами. Последний параметр называется TRest, в котором должен передаваться сам кортеж. Таким образом, есть возможность создавать кортежи с любым количеством параметров.
Для демонстрации этой функциональности напишем следующий код:
public class Tuple<T1, Т2, Т3, Т4, Т5, Т6, Т7, TRest>
Здесь последний параметр шаблона - сам тип кортежа, так что можно создать кортеж с любым числом элементов:
var tuple = Tuple.Create<string,string,string,int,int,int,double,
Tuple<int,int> ("Stephanie", "Alina", "Nagel", 2009, 6, 2, 1.37,
Tuple.Create<int,int>(52, 3490));
Структурное сравнение
Как массивы, так и кортежи реализуют интерфейс IStructuralEquatableи IStructuralComparable. Эти интерфейсы появились в.NET 4 и позволяют сравнивать не только ссылки, но и содержимое. Интерфейс реализован явно, поэтому при его использовании необходимо выполнять приведения массивов и кортежей. IStructuralEquatableслужит для определения того, имеют ли два кортежа или массива одинаковое содержимое, a IStructuralComparableприменяется для сортировки кортежей и массивов.
В следующем примере, демонстрирующем использование IStructuralEquatable, создан класс Person, который реализует интерфейс IEquatable. Этот интерфейс определяет строго типизированный метод Equals(), в котором сравниваются значения свойств FirstNameи LastName:
public class Person: IEquatable<Person>
{
public int Id {get; private set; }
public string FirstName {get; set;}
public string LastName {get; set;}
public override string ToString()
{
return String.Format("{0}, {1} {2}", Id, FirstName, LastName);
}
public override bool Equals(object obj)
{
if(obj == null) throw new ArgumentNullException("obj");
return Equals(obj as Person);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public bool Equals(Person other)
{
if (other == null) throw new ArgumentNullException("other");
return this.Id==other.Id && this.FirstName==other.FirstName &&
this.LastName == other.LastName;
}
}
Ниже создаются два массива элементов Person. Оба они содержат один и тот же объект Personс переменной по имени janetи два разных объекта Personс одинаковым содержимым. Операция сравнения!= возвращает true, потому что на самом деле это два разных массива, на которые ссылаются две переменные по имени persons1и persons2. Поскольку метод Equals() с одним параметром не переопределяется классом Array, то же самое случается и с операцией == при сравнении ссылок - они не совпадают.
var janet = new Person {FirstName = "Janet”, LastName = "Jackson"};
Person [] persons1 = { new Person
{
FirstName = "Michael",
LastName = "Jackson"
},
janet
};
Person[] persons2 = { new Person
{
FirstName = "Michael",
LastName = "Jackson"
},
janet
};
if (persons1!= persons2)
Console.WriteLine("разные ссылки");
Вызывая метод Equals(), определенный в IStructuralEquatableкак принимающий первый параметр типа objectи второй типа IEqualityComparer, можно определить, как именно должно выполняться сравнение, передавая объект, реализующий IEqualityComparer<T>. Реализация IEqualityComparerпо умолчанию предоставляется классом EqualityComparer<T>.В ней производится проверка, реализует ли тип интерфейс IEquatable, и вызывается IEquatable.Equals(). Если тип не реализует IEquatable, то для выполнения сравнения вызывается метод Equals() базового класса Object.
Класс Personреализует IEquatable<Person>, где содержимое объектов сравнивается, и оказывается, что массивы действительно включают одинаковое содержимое:
if ((persons1 as IStructuralEquatable).Equals(persons2,
EqualityComparer<Person>.Default))
{
Console.WriteLine("одинаковое содержимое");
}
Теперь будет показано, как то же самое можно сделать с применением кортежей. Ниже создаются два экземпляра кортежей с одинаковым содержимым. Разумеется, поскольку ссылки t1и t2указывают на два разный объекта, операция сравнения!= возвращает true:
var t1 = Tuple.Create<int, string>(l, "Stephanie");
var t2 = Tuple.Create<int, string>(l, "Stephanie");
if (t1!= t2) Console.WriteLine("не одинаковое содержимое");
Класс Tuple<>предоставляет два метода Equals(): один, переопределяющий реализацию базового класса Object, с objectв качестве параметра, а второй определен интерфейсом IStructuralEqualityComparer, с двумя параметрами - objectи IEqualityComparer. Как показано, другой кортеж может быть передан в первый метод. Чтобы получить ObjectEqualityComparer<object>для сравнения, этот метод использует EqualityComparer<object>.Default.Таким образом, каждый элемент в кортеже сравнивается за счет вызова метода Object.Equals(). Если для каждого элемента возвращается true, конечным результатом метода Equals() также будет true, что мы и видим здесь с одинаковыми значениями intи string:
if (t1.Equals(t2)) Console.WriteLine("одинаковое содержимое");
Можно также создать специальный интерфейс IEqualityComparer, как показано ниже на примере класса TupleComparer. В этом классе реализованы два метода - Equals() и GetHashCode() - интерфейса IEqualityComparer.
class TupleComparer: IEqualityComparer
{
public new bool Equals(object x, object y)
{
return x.EquaLs(y);
}
public int GetHashCode(object obj)
{
return obj.GetHashCode();
}
}
Реализация метода Equals() интерфейса IEqualityComparer требует модификатора new или неявной реализации интерфейса, потому что базовый класс Object также определяет статический метод Equals() с двумя параметрами.
TupleComparerиспользуется при передаче нового экземпляра методу Equals()класса Tuple<T1,Т2>.Метод Equals() класса Tupleвызывает метод Equals() класса TupleComparerдля каждого сравниваемого элемента. Поэтому с классом Tuple<T1, Т2>класс TupleComparerвызывается два раза для проверки эквивалентности всех элементов:
if (t1.Equals(t2, new TupleComparer()))
Console.WriteLine("равны после проверки с помощью TupleComparer");
Итоги
В этом разделе мы познакомились с нотацией C# для создания и использования простых, многомерных и зубчатых массивов. “За кулисами” механизма массивов C# применяется класс Array, и таким образом имеется возможность обращаться к свойствам и методам этого класса через переменные массива.
Было показано, как сортировать элементы массива с использованием интерфейсов IComparable и IComparer.
Вы познакомились с использованием и созданием перечислителей, с интерфейсами IEnumerable и IEnumerator, а также оператором yield. Кроме того, мы получили представление о кортежах - новом средстве.NET 4.