#c# #ienumerable #enumeration #ienumerator
#c# #ienumerable #перечисление #ienumerator
Вопрос:
У меня есть класс под названием GenericPermutations, который является одновременно и перечислимым, и перечислителем. Его задача состоит в том, чтобы взять упорядоченный список объектов и выполнить итерацию по каждой их перестановке по порядку.
Например, целочисленная реализация этого класса может повторять следующее:
GenericPermutations<int> p = new GenericPermutations<int>({ 1, 2, 3 });
p.nextPermutation(); // 123
p.nextPermutation(); // 132
p.nextPermutation(); // 213
// etc.
Итак, он перечислим в том смысле, что он содержит «список» вещей, которые вы можете перечислять. Это также перечислитель, потому что его работа включает в себя поиск следующей перестановки.
ПРОБЛЕМА: в настоящее время я пытаюсь интегрировать IEnumerator и IEnumerable с этим классом, и мне кажется, что это должно быть и то, и другое (вместо использования подкласса в качестве IEnumerable). До сих пор я избегал проблемы с попыткой получить из него два перечислителя, передавая новый объект GenericPermutation в GetEnumerator
методе.
Это плохая идея? Что-нибудь еще, что я должен рассмотреть?
Комментарии:
1. Если ваш перечислимый экземпляр также является единственным экземпляром для всех запрошенных из него перечислителей, как вы собираетесь сохранять состояние между ними?
2. Моя идея заключалась в том, что я создавал бы новую копию своего объекта каждый раз, когда запрашивается перечислитель, и передавал бы эту копию в качестве перечислителя. Таким образом, если запрашивается новый перечислитель, передается копия текущего перечислителя в его исходном состоянии.
Ответ №1:
Уменьшите вашу путаницу (?), используя общие версии IEnumerable
и IEnumerator
.
Перечислимая перестановка является IEnumerable<IEnumerable<T>>
. Итак, у вас может быть что-то вроде
IEnumerable<IEnumerable<T>> GetPermutations(IEnumerable<T> sequence)
{
return new Permuter<T>(sequence);
}
и
public class Permuter<T> : IEnumerable<IEnumerable<T>> { ... }
Кроме того, я видел не один случай, когда один тип реализовывал оба IEnumerable<T>
и IEnumerator<T>
; его метод GetEnumerator был простым return this;
.
Я думаю, что такой тип должен был бы быть struct , хотя, потому что, если бы это был класс, у вас возникли бы всевозможные проблемы, если бы вы вызвали GetEnumerator() во второй раз до завершения первого перечисления.
РЕДАКТИРОВАТЬ: Использование перестановочного
var permuter = GetPermutations(sequence);
foreach (var permutation in permuter)
{
foreach (var item in permutation)
Console.Write(item "; ");
Console.WriteLine();
}
Предполагая, что входная последовательность равна { 1, 2, 3}, на выходе получается
1; 2; 3;
1; 3; 2;
2; 1; 3;
2; 3; 1;
3; 1; 2;
3; 2; 1;
Редактировать:
Вот суперэффективная реализация, иллюстрирующая предложение:
public class Permuter<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> _sequence;
public Permuter(IEnumerable<T> sequence)
{
_sequence = sequence;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
foreach(var item in _sequence)
{
var remaining = _sequence.Except(Enumerable.Repeat(item, 1));
foreach (var permutation in new Permuter<T>(remaining))
yield return Enumerable.Repeat(item, 1).Concat(permutation);
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Комментарии:
1. Мне не нравится просто передавать
this
в методе GetEnumerator, потому что могут возникнуть проблемы во вложенных циклах for-each и других ситуациях. Ваше первое предложение выглядит интересным, не могли бы вы расширить его немного больше? Меня смущаетIEnumerable<IEnumerable<T>>
.2. Что вас смущает? Перестановки последовательности { 1, 2, 3} состоят из шести последовательностей ({ 1, 2, 3 }, { 1, 3, 2 }, и т.д.); это можно рассматривать как последовательность последовательностей. Я добавлю пример кода.
3. А, я понимаю, что вы имеете в виду. Однако мне никогда не придется перечислять сами последовательности, только те перестановки, которые их генерируют.
4. Теперь я в замешательстве. Последовательности являются перестановками … не так ли?
Ответ №2:
Для одного объекта возможно вести себя одновременно как IEnumerator<T>
и IEnumerable<T>
, но объекту, как правило, трудно сделать это таким образом, чтобы избежать причудливой семантики; если IEnumerator<T>
он не будет без состояния (например, пустой перечислитель, где MoveNext()
всегда возвращается false , или перечислитель с бесконечным повторением, где MoveNext()
ничего не делает, но всегда возвращает true, и Current
всегда возвращает одно и то же значение), каждый вызов GetEnumerator()
должен возвращать отдельный экземпляр объекта, и, вероятно, будет мало смысла в реализации этого экземпляра IEnumerable<T>
.
Наличие типа значения implementate IEnumerable<T>
и IEnumerator<T>
и возврат его GetEnumerator()
метода this
удовлетворяли бы требованию, чтобы каждый вызов GetEnumerator
возвращал отдельный экземпляр объекта, но наличие типов значений, реализующих изменяемые интерфейсы, обычно опасно. Если тип значения помещен в IEnuerator<T>
и никогда не распаковывается, он будет вести себя как объект типа класса, но нет реальной причины, по которой он не должен был быть просто объектом типа класса.
Итераторы в C # реализованы как объекты класса, которые реализуют оба IEnumerable<T>
и IEnumerator<T>
, но они включают в себя изрядную долю причудливой логики для обеспечения семантической корректности. Конечным результатом является то, что наличие одного объекта, реализующего оба интерфейса, обеспечивает небольшое улучшение производительности в обмен на изрядную сложность сгенерированного кода и некоторую семантическую причудливость в их IDisposable
поведении. Я бы не рекомендовал этот подход в любом коде, который должен быть удобочитаемым человеком; поскольку в IEnumerator<T>
и IEnumerable<T>
аспектах класса в основном используются разные поля, и поскольку объединенный класс должен иметь поле «thread-id», которое не понадобилось бы при использовании отдельных классов, улучшение производительности, которого можно достичь, используя один и тот же объект для реализации для обоих интерфейсов, ограничено. Возможно, стоит сделать, если добавление сложности компилятору обеспечит небольшое улучшение производительности миллионов подпрограмм итератора, но не стоит делать для повышения производительности одной подпрограммы.