Возврат перечислителей посредством yield return
С помощью оператора 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.