Как определить IEnumerable поведение по контракту? - PullRequest
4 голосов
/ 29 октября 2010

Рассмотрим 2 метода, которые возвращают IEnumerable:

    private IEnumerable<MyClass> GetYieldResult(int qtResult)
    {
        for (int i = 0; i < qtResult; i++)
        {
            count++;
            yield return new MyClass() { Id = i+1 };
        }
    }

    private IEnumerable<MyClass> GetNonYieldResult(int qtResult)
    {
        var result = new List<MyClass>();

        for (int i = 0; i < qtResult; i++)
        {
            count++;
            result.Add(new MyClass() { Id = i + 1 });
        }

        return result;
    }

Этот код показывает 2 различных поведения при вызове некоторого метода IEnumerable:

    [TestMethod]
    public void Test1()
    {
        count = 0;

        IEnumerable<MyClass> yieldResult = GetYieldResult(1);

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(2, count);//calling "First()" 2 times, yieldResult is created 2 times
        Assert.AreNotSame(firstGet, secondGet);//and created different instances of each list item
    }

    [TestMethod]
    public void Test2()
    {
        count = 0;

        IEnumerable<MyClass> yieldResult = GetNonYieldResult(1);

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(1, count);//as expected, it creates only 1 result set
        Assert.AreSame(firstGet, secondGet);//and calling "First()" several times will always return same instance of MyClass
    }

Просто выбрать, какое поведение я хочукогда мой код возвращает IEnumerables, но как я могу явно определить, что некоторый метод получает IEnumerable в качестве параметра, который создает один набор результатов независимо от того, сколько раз он вызывает метод «First ()».

Конечно, яя не хочу принудительно создавать все itens, и я хочу определить параметр как IEnumerable, чтобы сказать, что ни один элемент не будет включен или удален из коллекции.

РЕДАКТИРОВАТЬ: Просто чтобы прояснить вопросне о том, как работает yield или почему IEnumerable может возвращать разные экземпляры для каждого вызова.Вопрос в том, как указать, что параметр должен быть коллекцией «только для поиска», которая возвращает одни и те же экземпляры MyClass, когда я вызываю такие методы, как «First ()» или «Take (1)» несколько раз.

Есть идеи?

Заранее спасибо!

Ответы [ 5 ]

2 голосов
/ 29 октября 2010

Конечно, я не хочу заставлять создавать все itens без необходимости

В этом случае вам нужно разрешить методу создавать их по требованию, и если объектысозданные по требованию (и без какой-либо формы кэша), они будут различными объектами (по крайней мере в том смысле, что они являются разными ссылками - определение равенства по умолчанию для объектов, не имеющих значения).

Если ваши объекты по своей сути уникальны (то есть они не определяют некоторое равенство, основанное на значениях), то каждый вызов new создаст отдельный объект (независимо от параметров конструктора).

Так что ответ на

но как я могу явно определить, что какой-то метод получает IEnumerable в качестве параметра, который создает один набор результатов независимо от того, сколько раз он вызывает метод «First ()».

«Вы не можете», за исключением того, что создаете один набор объектов и повторно возвращаете один и тот же набор, или , определяя равенство как нечто иное.


Дополнительно (на основе комментариев).Если вы действительно хотите иметь возможность воспроизвести (из-за отсутствия лучшего термина) тот же набор объектов, не создавая целую коллекцию, которую вы могли бы кэшировать, вы уже сгенерировали и воспроизведите это в первую очередь.Что-то вроде:

private static List<MyData> cache = new List<MyData>();
public IEnumerable<MyData> GetData() {
  foreach (var d in cache) {
    yield return d;
  }

  var position = cache.Count;

  while (maxItens < position) {
    MyData next = MakeNextItem(position);
    cache.Add(next);
    yield return next;
  }
}

Я предполагаю, что было бы возможно построить такую ​​оболочку для кэширования также вокруг итератора (while станет foreach поверх основного итератора, но вам нужно будет кэшировать этот итераторили Skip в требуемую позицию, если вызывающая программа итеративно вышла за пределы кэширования List).

NB любой подход к кэшированию будет трудно сделать потокобезопасным.

1 голос
/ 29 октября 2010

Вы можете смешивать предложения, вы можете реализовать класс-обертку, основанный на обобщениях, который принимает IEnumerable и возвращает новый, который создает кеш для каждого следующего и повторно использует частичный кеш при необходимости для дальнейших перечислений. Это нелегко, но создавать объекты (по правде говоря, только для итераторов, которые создают объекты на лету) только один раз и по мере необходимости. Сложнее всего быть уверенным, когда переключаться с частичного кэша обратно на исходный перечислитель и как сделать его транзакционным (непротиворечивым).

Обновление с проверенным кодом:

public interface ICachedEnumerable<T> : IEnumerable<T>
{
}

internal class CachedEnumerable<T> : ICachedEnumerable<T>
{
    private readonly List<T> cache = new List<T>();
    private readonly IEnumerator<T> source;
    private bool sourceIsExhausted = false;

    public CachedEnumerable(IEnumerable<T> source)
    {
        this.source = source.GetEnumerator();
    }

    public T Get(int where)
    {
        if (where < 0)
            throw new InvalidOperationException();
        SyncUntil(where);
        return cache[where];
    }

    private void SyncUntil(int where)
    {
        lock (cache)
        {
            while (where >= cache.Count && !sourceIsExhausted)
            {
                sourceIsExhausted = source.MoveNext();
                cache.Add(source.Current);
            }
            if (where >= cache.Count)
                throw new InvalidOperationException();
        }
    }

    public bool GoesBeyond(int where)
    {
        try
        {
            SyncUntil(where);
            return true;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new CachedEnumerator<T>(this);
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return new CachedEnumerator<T>(this);
    }

    private class CachedEnumerator<T> : IEnumerator<T>, System.Collections.IEnumerator
    {
        private readonly CachedEnumerable<T> parent;
        private int where;

        public CachedEnumerator(CachedEnumerable<T> parent)
        {
            this.parent = parent;
            Reset();
        }

        public object Current
        {
            get { return Get(); }
        }

        public bool MoveNext()
        {
            if (parent.GoesBeyond(where))
            {
                where++;
                return true;
            }
            return false;
        }

        public void Reset()
        {
            where = -1;
        }

        T IEnumerator<T>.Current
        {
            get { return Get(); }
        }

        private T Get()
        {
            return parent.Get(where);
        }

        public void Dispose()
        {
        }
    }
}

public static class CachedEnumerableExtensions
{
    public static ICachedEnumerable<T> AsCachedEnumerable<T>(this IEnumerable<T> source)
    {
        return new CachedEnumerable<T>(source);
    }
}

Теперь вы можете добавить новый тест, который показывает, что он работает:

    [Test]
    public void Test3()
    {
        count = 0;

        ICachedEnumerable<MyClass> yieldResult = GetYieldResult(1).AsCachedEnumerable();

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(1, count);//calling "First()" 2 times, yieldResult is created 2 times
        Assert.AreSame(firstGet, secondGet);//and created different instances of each list item
    }

Код будет включен в мой проект http://github.com/monoman/MSBuild.NUnit, может позже появиться и в проекте Managed.Commons *

1 голос
/ 29 октября 2010

Я уже некоторое время пытаюсь найти элегантное решение проблемы. Мне бы хотелось, чтобы разработчики фреймворков добавили немного «IsImmutable» или аналогичного метода получения свойств в IEnumerable, чтобы можно было легко добавить метод расширения Evaluate (или аналогичный), который ничего не делает для IEnumerable, который уже находится в его «полностью оцененной "состояние.

Однако, поскольку этого не существует, вот лучшее, что я смог придумать:

  1. Я создал свой собственный интерфейс, который предоставляет свойство неизменяемости, и я реализую его во всех моих пользовательских типах коллекций.
  2. Моя реализация Evaluate метод расширения знает об этом новый интерфейс, а также неизменность подмножества соответствующие типы BCL, которые я потребляю чаще всего.
  3. я не возвращаюсь "сырые" типы коллекций BCL из моего API для повышения эффективности моего метода Evaluate (по крайней мере, при работе с моим собственным кодом).

Это довольно глупо, но это наименее навязчивый подход, который я смог найти до сих пор, чтобы решить проблему, позволяющую потребителю IEnumerable создавать локальную копию, только когда это действительно необходимо. Я очень надеюсь, что ваш вопрос выманивает из дерева более интересные решения ...

1 голос
/ 29 октября 2010

Если я неправильно вас понял, ваш вопрос может быть вызван неправильным пониманием. Ничто не возвращает IEnumerable. В первом случае возвращается Enumerator, который реализует foreach, позволяя вам получать экземпляры MyClass, по одному за раз. Он (возвращаемое значение функции) типизируется как IEnumerable, чтобы указать, что он поддерживает поведение foreach (и некоторые другие)

Вторая функция фактически возвращает List, который, конечно, также поддерживает IEnumerable (foreach поведение). Но это фактическая конкретная коллекция объектов MyClass, созданная вызванным вами методом (второй)

Первый метод вообще не возвращает никаких объектов MyClass, он возвращает тот объект перечислителя, который создается платформой dotNet и кодируется за кулисами для создания экземпляра нового объекта MyClass при каждой итерации по нему.

РЕДАКТИРОВАТЬ: Подробнее Более важное различие заключается в том, хотите ли вы, чтобы элементы удерживались на месте для вас внутри класса во время итерации, или хотите, чтобы они создавались для вас при итерации.

Другое соображение состоит в том, что ... предметы, которые вы хотите вернуть, уже существуют где-то еще? т.е. собирается ли этот метод перебирать набор (или отфильтрованное подмножество) некоторой существующей коллекции? или это создание предметов на лету? если последнее, имеет ли это значение имеет значение , если предмет является точно таким же экземпляром каждый раз, когда вы его "получаете"? Для объектов, определенных для существующих вещей, которые можно назвать сущностью - что-то с определенной идентичностью , вы, вероятно, хотите, чтобы последовательные выборки возвращали один и тот же экземпляр.

Но, может быть, другой экземпляр с таким же состоянием полностью эквивалентен? (Это будет называться типом значения объект, такой как номер телефона, или адрес, или точка на экране. Такие объекты не имеют идентичности, за исключением того, что подразумевается их состоянием. В этом последнем случае это не имеет значения, возвращает ли перечислитель один и тот же экземпляр или только что созданную идентичную копию каждый раз, когда вы его «получаете» ... Такие объекты, как правило, являются неизменяемыми, они одинаковы, они остаются одинаковыми и функционируют одинаково.

0 голосов
/ 29 октября 2010

Затем вам нужно кэшировать результат, IEnumerable всегда перезапускается, когда вы вызываете что-то, что перебирает его. Я склонен использовать:

private List<MyClass> mEnumerable;
public IEnumerable<MyClass> GenerateEnumerable()
{
    mEnumerable = mEnumerable ?? CreateEnumerable()
    return mEnumerable;
}
private List<MyClass> CreateEnumerable()
{
    //Code to generate List Here
}

С другой стороны (скажем, для вашего примера), вы можете иметь вызов ToList в конце, здесь будет итерация и создание списка, который будет сохранен, а yieldResult все равно будет IEnumerable без проблем.

[TestMethod]
public void Test1()
{
    count = 0;


    IEnumerable<MyClass> yieldResult = GetYieldResult(1).ToList();

    var firstGet = yieldResult.First();
    var secondGet = yieldResult.First();

    Assert.AreEqual(1, firstGet.Id);
    Assert.AreEqual(1, secondGet.Id);

    Assert.AreEqual(2, count);//calling "First()" 2 times, yieldResult is created 1 time
    Assert.AreSame(firstGet, secondGet);
}
...