Повернути всі перелічні дані з віддачею одразу; не перебираючи наскрізь


164

У мене є така функція, щоб отримати помилки перевірки для картки. Моє запитання стосується роботи з GetErrors. Обидва способи мають однаковий тип повернення IEnumerable<ErrorInfo>.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    var errors = GetMoreErrors(card);
    foreach (var e in errors)
        yield return e;

    // further yield returns for more validation errors
}

Чи можливо повернути всі помилки, GetMoreErrorsне перераховуючи їх?

Думати про це, мабуть, дурне питання, але я хочу переконатися, що я не помиляюся.


Я радий (і цікаво!), Коли з’являється більше запитань щодо прибутку - я сам цього не дуже розумію. Не дурне питання!
Джош Джордан

Що таке GetCardProductionValidationErrorsFor?
Ендрю Заєць

4
що не так у поверненні GetMoreErrors (картка); ?
Сем Шафран

10
@Sam: "подальший прибуток приносить більше помилок перевірки"
Джон Скіт

1
З точки зору неоднозначної мови, одне питання полягає в тому, що метод не може знати, чи є щось, що реалізує і T, і IEnumerable <T>. Тому вам потрібна інша конструкція в урожайності. Але, безумовно, було б добре це зробити. Foo, прибутковість прибутку foo, можливо, де foo реалізує IEnumerable <T>?
Вільям Йокуш

Відповіді:


141

Це, безумовно, не дурне питання, і це те, що F # підтримує yield!для цілої колекції проти yieldодного предмета. (Це може бути дуже корисно в плані рекурсії хвоста ...)

На жаль, це не підтримується в C #.

Однак якщо у вас є кілька методів кожного повернення IEnumerable<ErrorInfo>, ви можете скористатися Enumerable.Concatдля спрощення коду:

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetMoreErrors(card).Concat(GetOtherErrors())
                              .Concat(GetValidationErrors())
                              .Concat(AnyMoreErrors())
                              .Concat(ICantBelieveHowManyErrorsYouHave());
}

Існує одна дуже важлива різниця між двома реалізаціями: цей буде викликати всі методи негайно , хоча він буде використовувати лише повернені ітератори по черзі. Ваш наявний код буде чекати, поки він не перегляне все, GetMoreErrors()перш ніж він навіть запитає про наступні помилки.

Зазвичай це не важливо, але варто зрозуміти, що буде, коли.


3
У Уеса Дайєра є цікава стаття, в якій згадується цей візерунок. blogs.msdn.com/wesdyer/archive/2007/03/23/…
Йоханнес

1
Незначна корекція для перехожих - це System.Linq.Enumeration.Concat <> (перший, другий). Не IEnumeration.Concat ().
redcalx

@ the-locster: Я не впевнений, що ти маєш на увазі. Це, безумовно, численніше, ніж перерахування. Чи можете ви пояснити свій коментар?
Джон Скіт

@Jon Skeet - Що саме ти маєш на увазі, що він негайно викликатиме методи? Я провів тест і, схоже, це повністю відкладає виклики методу, поки щось насправді не повториться. Код тут: pastebin.com/0kj5QtfD
Стівен Окслі

5
@Steven: Ні. Це викликає методи, але у вашому випадку GetOtherErrors()(тощо) відкладає свої результати (як вони реалізовані за допомогою блоків ітератора). Спробуйте змінити їх, щоб повернути новий масив чи щось подібне, і ви побачите, що я маю на увазі.
Джон Скіт

26

Ви можете налаштувати всі такі джерела помилок (назви методів, запозичені у відповіді Джона Скіта).

private static IEnumerable<IEnumerable<ErrorInfo>> GetErrorSources(Card card)
{
    yield return GetMoreErrors(card);
    yield return GetOtherErrors();
    yield return GetValidationErrors();
    yield return AnyMoreErrors();
    yield return ICantBelieveHowManyErrorsYouHave();
}

Потім ви можете одночасно повторювати їх.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    foreach (var errorSource in GetErrorSources(card))
        foreach (var error in errorSource)
            yield return error;
}

Крім того, ви можете вирівняти джерела помилок за допомогою SelectMany.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetErrorSources(card).SelectMany(e => e);
}

Виконання методів у GetErrorSourcesтакож буде відкладено.


17

Я придумав швидкий yield_фрагмент:

Анімація використання_ знімка використання_

Ось фрагмент XML:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Author>John Gietzen</Author>
      <Description>yield! expansion for C#</Description>
      <Shortcut>yield_</Shortcut>
      <Title>Yield All</Title>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="true">
          <Default>items</Default>
          <ID>items</ID>
        </Literal>
        <Literal Editable="true">
          <Default>i</Default>
          <ID>i</ID>
        </Literal>
      </Declarations>
      <Code Language="CSharp"><![CDATA[foreach (var $i$ in $items$) yield return $i$$end$;]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

2
Як це відповідь на запитання?
Ян Кемп

1
@Ian, так вам потрібно зробити вкладений прибуток в C #. Немає yield!, як у F #.
Джон Гітцен

це не відповідь на питання
divyang4481

8

Я не бачу нічого поганого у вашій функції, я б сказав, що вона робить те, що ви хочете.

Подумайте про вихід, як що повертає елемент у підсумковому перерахуванні кожного разу, коли він викликається, тому коли ви маєте його в циклі foreach, як це, кожен раз, коли він викликається, він повертає 1 елемент. У вас є можливість помістити умовні висловлювання в передбачення, щоб фільтрувати набір результатів. (просто не поступаючись вашим критеріям виключення)

Якщо ви додасте наступні уроки пізніше в методі, він буде продовжувати додавати 1 елемент до перерахунку, що дозволяє робити такі речі, як ...

public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
{
  foreach (IEnumerable<string> list in lists)
  {
    foreach (string s in list)
    {
      yield return s;
    }
  }
}

4

Я здивований, що ніхто не думав рекомендувати простий метод розширення, IEnumerable<IEnumerable<T>>щоб змусити цей код зберігати відкладене виконання. Я прихильник відкладеного виконання з багатьох причин, одна з них полягає в тому, що слід пам'яті невеликий навіть для величезних монґозних перелічників.

public static class EnumearbleExtensions
{
    public static IEnumerable<T> UnWrap<T>(this IEnumerable<IEnumerable<T>> list)
    {
        foreach(var innerList in list)
        {
            foreach(T item in innerList)
            {
                yield return item;
            }
        }
    }
}

І ви могли б використовувати його у вашому випадку так

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return DoGetErrors(card).UnWrap();
}

private static IEnumerable<IEnumerable<ErrorInfo>> DoGetErrors(Card card)
{
    yield return GetMoreErrors(card);

    // further yield returns for more validation errors
}

Аналогічно, ви можете покінчити з функцією обгортки навколо DoGetErrorsта просто перейти UnWrapна виклик.


2
Напевно, ніхто не замислювався над методом розширення, оскільки він DoGetErrors(card).SelectMany(x => x)робить те саме і зберігає відкладену поведінку. Що саме це пропонує Адам у своїй відповіді .
huysentruitw

3

Так, можна повернути всі помилки відразу. Просто поверніть List<T>або ReadOnlyCollection<T>.

Поверненням IEnumerable<T>ви повертаєте послідовність чогось. На поверхні, яка може здатися ідентичною поверненню колекції, але є різниця, слід пам’ятати.

Колекції

  • Абонент може бути впевнений, що і колекція, і всі предмети будуть існувати при поверненні колекції. Якщо колекцію потрібно створювати за виклик, повернення колекції - це дійсно погана ідея.
  • Більшість колекцій можна змінити після повернення.
  • Колекція має обмежений розмір.

Послідовності

  • Можна перерахувати - і це майже все, що ми можемо сказати точно.
  • Саму повернуту послідовність неможливо змінити.
  • Кожен елемент може бути створений як частина запуску послідовності (тобто повернення IEnumerable<T>дозволяє проводити ледачу оцінку, повернення List<T>не робить).
  • Послідовність може бути нескінченною і, таким чином, залишити її абоненту, щоб вирішити, скільки елементів потрібно повернути.

Повернення колекції може призвести до необґрунтованих накладних витрат, якщо все, що дійсно потрібно клієнту, перераховувати через нього, оскільки ви заздалегідь виділяєте структури даних для всіх елементів. Крім того, якщо ви делегуєте інший метод, який повертає послідовність, то захоплення його як колекції передбачає додаткове копіювання, і ви не знаєте, скільки предметів (і, отже, скільки накладних витрат) це може залучати. Таким чином, повернути колекцію корисно лише тоді, коли вона вже є і може бути повернута безпосередньо без копіювання (або загорнута як лише для читання). У всіх інших випадках послідовність є кращим вибором
Павло Мінаєв

Я згоден, і якщо у вас склалося враження, що я сказав, що повернення колекції - це завжди гарна ідея, ти пропустив мій погляд. Я намагався підкреслити той факт, що між поверненням колекції та поверненням послідовності існують відмінності. Я спробую зробити це зрозумілішим.
Брайан Расмуссен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.