Обнаружение множества «государственных машин»

17

Я только что прочитал интересную статью под названием Получение слишком мило с C # yield return

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

Например, вы можете изменить DoubleXValue (из статьи) на что-то вроде:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

Вопрос 1) Есть ли лучший способ сделать это?

Вопрос 2) Это то, что я должен беспокоиться при создании API?

ConditionRacer
источник
1
Вы должны использовать интерфейс, а не бетон, а также бросить вниз, ICollection<T>это был бы лучший выбор, поскольку не все коллекции List<T>. Например, массивы Point[]реализуют, IList<T>но это не так List<T>.
Тревор Пилли
3
Добро пожаловать в проблему с изменчивыми типами. Предполагается, что Ienumerable является неизменным типом данных, поэтому реализации-мутанты являются нарушением LSP. Эта статья является идеальным объяснением опасности побочных эффектов, поэтому попробуйте написать ваши типы C # как можно более свободными от побочных эффектов, и вам никогда не нужно об этом беспокоиться.
Джимми Хоффа
1
@JimmyHoffa IEnumerableявляется неизменным в том смысле, что вы не можете изменять коллекцию (добавлять / удалять) при ее перечислении, однако, если объекты в списке являются изменяемыми, то разумно изменять их при перечислении списка.
Тревор Пилли
@TrevorPilley Я не согласен. Если ожидается, что коллекция будет неизменной, то от потребителя ожидается, что участники не будут видоизменены внешними действующими лицами, что будет названо побочным эффектом и нарушает ожидание неизменности. Нарушает ПОЛА и имплицитно ЛСП.
Джимми Хоффа
Как насчет использования ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Он не определяет, сгенерирован ли он урожайностью, но вместо этого делает его неактуальным.
luiscubal

Ответы:

22

Ваш вопрос, насколько я понимаю, кажется, основан на неверной предпосылке. Позвольте мне посмотреть, смогу ли я восстановить аргументацию:

  • Связанная статья описывает, как автоматически сгенерированные последовательности демонстрируют «ленивое» поведение, и показывает, как это может привести к противоречивому результату.
  • Поэтому я могу определить, будет ли данный экземпляр IEnumerable демонстрировать это ленивое поведение, проверив, генерируется ли он автоматически.
  • Как мне это сделать?

Проблема в том, что вторая предпосылка неверна. Даже если бы вы могли определить, был ли данный IEnumerable результатом преобразования блока итератора (и да, есть способы сделать это), это не помогло бы, потому что предположение неверно. Давайте проиллюстрируем почему.

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

Хорошо, у нас есть четыре метода. S1 и S2 - автоматически генерируемые последовательности; S3 и S4 - сгенерированные вручную последовательности. Теперь предположим, что у нас есть:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

Результат для S1 и S4 будет 0; каждый раз, когда вы перечисляете последовательность, вы получаете новую ссылку на созданную букву M. Результат для S2 и S3 будет 100; каждый раз, когда вы перечисляете последовательность, вы получаете ту же ссылку на M, что и в прошлый раз. То, генерируется ли код последовательности автоматически или нет, является ортогональным к вопросу о том, имеют ли перечисленные объекты ссылочную идентичность или нет. Эти два свойства - автоматическая генерация и ссылочная идентичность - на самом деле не имеют ничего общего друг с другом. Статья, на которую вы ссылаетесь, несколько их объединяет.

Если поставщик последовательности не задокументирован как всегда предлагающий объекты, имеющие ссылочную идентичность , неразумно полагать, что это так.

Эрик Липперт
источник
2
Мы все знаем, что у вас будет правильный ответ, это данность; но если бы вы могли добавить немного больше определения в термин «ссылочная идентичность», это было бы хорошо, я читаю его как «неизменяемый», но это потому, что я сопоставляю неизменяемый в C # с новым объектом, возвращаемым каждый тип get или value, который, я думаю, это то же самое, что вы имеете в виду. Хотя предоставленную неизменность можно получить разными способами, это единственный рациональный способ в C # (о котором я знаю, вы, возможно, хорошо знаете способы, о которых я не знаю).
Джимми Хоффа
2
@JimmyHoffa: две ссылки на объекты x и y имеют «ссылочную идентичность», если Object.ReferenceEquals (x, y) возвращает true.
Эрик Липперт
Всегда приятно получить ответ прямо из источника. Спасибо, Эрик.
ConditionRacer
10

Я думаю, что ключевой концепцией является то, что вы должны рассматривать любую IEnumerable<T>коллекцию как неизменную : вы не должны изменять объекты из нее, вы должны создать новую коллекцию с новыми объектами:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(Или напишите то же самое более кратко, используя LINQ Select().)

Если вы действительно хотите изменить элементы в коллекции, не используйте IEnumerable<T>, не используйте IList<T>(или, возможно, IReadOnlyList<T>если вы не хотите изменять саму коллекцию, только элементы в ней, и вы находитесь на .Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

Таким образом, если кто-то попытается использовать ваш метод с IEnumerable<T>, он потерпит неудачу во время компиляции.

svick
источник
1
Настоящий ключ, как вы сказали, возвращает новый список с изменениями, когда вы хотите что-то изменить. Ienumerable как тип совершенно жизнеспособен и не имеет причин для изменения, это просто методика его использования, которую нужно понять, как вы сказали. +1 за упоминание о том, как работать с неизменяемыми типами.
Джимми Хоффа
1
В некоторой степени связанный - вы должны также обращаться с каждым IEnumerable, как если бы это было сделано с помощью отложенного выполнения. То, что может быть правдой в то время, когда вы извлекаете свою ссылку IEnumerable, может не быть правдой, когда вы фактически перечисляете ее. Например, возвращение оператора LINQ внутри блокировки может привести к некоторым интересным неожиданным результатам.
Дэн Лайонс
@JimmyHoffa: я согласен. Я иду на шаг дальше и стараюсь не повторяться IEnumerableболее одного раза. Необходимость сделать это не раз может быть устранена путем вызова ToList()и итерации по списку, хотя в конкретном случае, показанном OP, я предпочитаю решение svick о том, что DoubleXValue также возвращает IEnumerable. Конечно, в реальном коде я бы, вероятно, использовал LINQдля генерации такого типа IEnumerable. Тем не менее, выбор Свика yieldимеет смысл в контексте вопроса ОП.
Брайан
@ Брайан Да, я не хотел слишком сильно менять код, чтобы понять мою точку зрения. В реальном коде я бы использовал Select(). И в отношении нескольких итераций IEnumerable: ReSharper полезен в этом, потому что он может предупредить вас, когда вы это сделаете.
svick
5

Я лично считаю, что проблема, о которой говорилось в этой статье, связана с чрезмерным использованием IEnumerableинтерфейса многими разработчиками C # в наши дни. Кажется, он стал типом по умолчанию, когда вам требуется набор объектов - но есть много информации, которая IEnumerableпросто не говорит вам ... крайне важно: была ли она усовершенствована *.

Если вы обнаружите, что вам нужно определить, IEnumerableдействительно ли объект является a IEnumerableили чем-то еще, абстракция просочилась, и вы, вероятно, находитесь в ситуации, когда ваш тип параметра немного слишком свободный. Если вам требуется дополнительная информация, которая IEnumerableсама по себе не предоставлена, сделайте это требование явным, выбрав один из других интерфейсов, таких как ICollectionили IList.

Редактировать для downvoters Пожалуйста, вернитесь и внимательно прочитайте вопрос. ОП запрашивает способ обеспечения того, чтобы IEnumerableэкземпляры были материализованы перед передачей его методу API. Мой ответ на этот вопрос. Существует более широкая проблема, касающаяся правильного способа использования IEnumerable, и, конечно, ожидания кода, вставленного OP, основаны на неправильном понимании этой более широкой проблемы, но OP не спрашивал, как правильно использовать IEnumerableили как работать с неизменяемыми типами. , Нечестно опровергать меня за то, что я не отвечаю на вопрос, который не был поставлен.

* Я использую термин reify, другие используют stabilizeили materialize. Я не думаю, что сообщество уже приняло общую конвенцию.

MattDavey
источник
2
Одна проблема с ICollectionи IListзаключается в том, что они изменчивы (или, по крайней мере, выглядят так). IReadOnlyCollectionи IReadOnlyListиз .Net 4.5 решить некоторые из этих проблем.
svick
Пожалуйста, объясните отрицательный голос?
MattDavey
1
Я не согласен, что вы должны изменить типы, которые вы используете. Ienumerable - отличный тип, и он часто может иметь преимущество неизменности. Я думаю, что настоящая проблема в том, что люди не понимают, как работать с неизменяемыми типами в C #. Не удивительно, что это язык с очень большим состоянием, поэтому это новая концепция для многих разработчиков C #.
Джимми Хоффа
Только ienumerable допускает отложенное выполнение, поэтому запрещение его в качестве параметра удаляет одну целую особенность C #
Джимми Хоффа
2
Я проголосовал за это, потому что я думаю, что это неправильно. Я думаю, что в духе SE, чтобы люди не оценивали неверную информацию, мы все должны проголосовать за эти ответы. Может быть, я не прав, вполне возможно, что я не прав, а вы правы. Это зависит от более широкого сообщества, чтобы решить путем голосования. Извините, что вы принимаете это лично, если у вас есть какое-то утешение, у меня было много отрицательных ответов, которые я написал и кратко удалил, потому что я согласен, что сообщество считает, что это неправильно, поэтому я вряд ли прав.
Джимми Хоффа