Есть ли канонический способ «исправить» «динамический» IEnumerable? - PullRequest
6 голосов
/ 09 февраля 2012

IEnumerable не гарантирует, что двойное перечисление даст один и тот же результат. На самом деле, довольно просто создать пример, где myEnumerable.First() возвращает разные значения при двойном выполнении:

class A {
    public A(string value) { Value = value; }
    public string Value {get; set; } 
}

static IEnumerable<A> getFixedIEnumerable() {
    return new A[] { new A("Hello"), new A("World") };
}

static IEnumerable<A> getDynamicIEnumerable() {
    yield return new A("Hello");
    yield return new A("World");
}

static void Main(string[] args)
{
    IEnumerable<A> fix = getFixedIEnumerable();
    IEnumerable<A> dyn = getDynamicIEnumerable();

    Console.WriteLine(fix.First() == fix.First()); // true
    Console.WriteLine(dyn.First() == dyn.First()); // false
}

Это не просто академический пример: использование популярного from ... in ... select new A(...) создаст именно эту ситуацию. Это может привести к неожиданному поведению:

fix.First().Value = "NEW";
Console.WriteLine(fix.First().Value); // prints NEW

dyn.First().Value = "NEW";
Console.WriteLine(dyn.First().Value); // prints Hello

Я понимаю, почему это происходит. Я также знаю, что это можно исправить, выполнив ToList() в Enumerable или переопределив == для класса A. Это не мой вопрос.

Вопрос: когда вы пишете метод, который принимает произвольный IEnumerable, и вы хотите, чтобы свойство оценивалось только один раз (а затем ссылки были «фиксированными»), каков канонический способ сделать это? ToList(), кажется, в основном используется, но если источник уже зафиксирован (например, если источник является массивом), ссылки копируются в список (без необходимости, поскольку все, что мне нужно, это свойство fixed). Есть что-то более подходящее или ToList() "каноническое" решение для этой проблемы?

Ответы [ 5 ]

3 голосов
/ 09 февраля 2012

ToList вполне определенно подходит.

Он имеет некоторые оптимизации: если входное перечисление является ICollection (например, массив или список), он вызовет ICollection.CopyTo для копированиясбор в массив, а не фактическое перечисление - поэтому стоимость вряд ли будет существенной, если только сбор не огромен.

ИМХО, в общем случае для большинства методов лучше возвращать ICollection<T> или IList<T>, чемIEnumerable<T>, , если только вы не захотите намекнуть потребителям метода, что реализация может использовать ленивую оценку (например, yield).

В тех случаях, когда метод должен возвращать неизменный список, возвращайтеОболочка только для чтения (ReadOnlyCollection<T>), например, путем вызова ToList().AsReadOnly(), но все равно возвращает тип интерфейса IList<T>.

Если вы следуете этому руководству, пользователям метода никогда не понадобится ненужный вызовToList.

2 голосов
/ 09 февраля 2012

Интерфейс IEnumerable просто дает "способ" перебирать элементы (создание IEnumerable из запроса linq является прекрасным примером, поскольку IEnumerable подобен запросу SQL для базы данных) Он всегда будет динамическим, пока вы не сохраните результат в ICollection (ToList, ToArray), поэтому итерация обрабатывается до конца, а результат сохраняется «фиксированным» способом.

2 голосов
/ 09 февраля 2012

Вы имеете в виду, если ваша функция принимает параметр, и вы хотите убедиться, что параметр не ленив, потому что вам нужно повторять его более одного раза в методе? Вместо этого я обычно делаю параметр ICollection в этом случае, поэтому ответственность за повторное перечисление перечисляемого объекта возлагается на ленивого.

1 голос
/ 09 февраля 2012

Самая распространенная проблема сформулирована Джоэлем Спольски в его Законе слабых абстракций .Имея параметр IEnumerable в вашем методе, вы ожидаете объект, который можно просто перечислить и ничего более.Но тем не менее вы заботитесь о переданной коллекции, будь то «фиксированная» или «динамическая».Вы можете видеть, что абстракция, сделанная IEnumerable, просочилась.Не существует решения, которое подходит для всех случаев.В вашем случае вы можете передать List<T> или T[] (или некоторые другие типы, которые, как вы ожидаете, будут фактическими типами для параметров в вашем методе) вместо IEnumerable.Самый распространенный совет - реализовать свою абстракцию и разработать свой код в соответствии с ней.

0 голосов
/ 09 февраля 2012

Из того, как вы объяснили свой вопрос, кажется, что вы хотите перечислить последовательность несколько раз и получать один и тот же порядок каждый раз.Это подразумевает, что вы либо держитесь за ссылку на последовательность, чтобы выполнить итерации позже, либо выполняете сравнения несколько раз в одном и том же методе.

Вообще говоря, при принятии IEnumerable<T>, который вы имеете в видусохранить ссылку или манипулировать более одного раза, безопаснее скопировать последовательность во внутреннее представление.Я предпочитаю использовать расширение ToList(), так как оно очень хорошо понято и, возможно, является самым простым набором с предсказуемым порядком.

Внутренне .NET Framework стремится использовать List<T> всякий раз, когда "общий"Сбор обязателен.

...