Виявлення численних "державних машин"


17

Я щойно прочитав цікаву статтю під назвою " Надто мило з поверненням урожаю" #

Це змусило мене замислитися, що найкращий спосіб виявити, чи є IEnumerable фактично перелічуваною колекцією, чи це машина машини, створена за допомогою ключового слова дохід.

Наприклад, ви можете змінити 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?


1
Ви повинні використовувати інтерфейс, а не конкретний, а також опускатись нижче, ICollection<T>було б кращим вибором, оскільки не всі колекції є List<T>. Наприклад, масиви Point[]реалізують, IList<T>але це не так List<T>.
Тревор Піллі

3
Ласкаво просимо до проблеми із змінними типами. Безлічі неявно повинні вважатись незмінним типом даних, тому мутація реалізацій є порушенням LSP. Ця стаття є ідеальним поясненням небезпеки побічних ефектів, тому практикуйте писати свої типи C #, щоб бути максимально вільним від побічних ефектів, і вам ніколи не потрібно турбуватися з цього приводу.
Джиммі Хоффа

1
@JimmyHoffa IEnumerableнезмінна в тому сенсі, що ви не можете змінювати колекцію (додавати / видаляти) під час її перерахування, однак якщо об'єкти зі списку є змінними, цілком розумно їх змінювати під час перерахування списку.
Тревор Піллі

@TrevorPilley Я не згоден. Якщо колекція буде непорушною, очікування споживачів полягає в тому, що учасники не будуть мутувати сторонніми суб'єктами, що можна назвати побічним ефектом і порушувати очікування імутаблітету. Порушує POLA та неявно LSP.
Джиммі Хоффа

Як щодо використання 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 # з новим об'єктом, що повертається кожен тип отримання або значення, що я думаю це те саме, про що ви посилаєтесь. Хоча надана незмінність може бути різними іншими способами, це єдиний раціональний спосіб у 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>, він не вдасться під час компіляції.


1
Справжній ключ, як ви сказали, поверніть нове безліч змін, коли ви хочете щось змінити. Незчинна кількість як тип є цілком життєздатною і не має підстав змінюватись, це просто розуміння техніки використання, як ви сказали. +1, щоб згадати, як працювати з непорушними типами.
Джиммі Хоффа

1
Дещо пов'язане - ви також повинні ставитися до кожного IEnumerable так, ніби це робиться за допомогою відкладеного виконання. Що може бути істинним під час отримання вашої IEnumerable посилання, можливо, не відповідає дійсності, коли ви насправді перераховуєте через нього. Наприклад, повернення оператора LINQ всередині блокування може призвести до цікавих несподіваних результатів.
Ден Ліон

@JimmyHoffa: Я згоден. Я йду на крок далі і намагаюся не раз повторювати її IEnumerable. Необхідність робити це не раз можна усунути, зателефонувавши ToList()і повторивши список, хоча в конкретному випадку, показаному ОП, я вважаю за краще рішення Svick про те, що DoubleXValue також повертає IEnumerable. Звичайно, в реальному коді я, мабуть, використовував би LINQдля створення цього типу IEnumerable. Однак вибір svick yieldмає сенс у контексті питання про ОП.
Брайан

@Brian Так, я не хотів занадто сильно змінювати код, щоб зробити свою думку. У реальному коді я б використав Select(). Щодо декількох ітерацій IEnumerable: ReSharper корисний для цього, оскільки він може попереджати вас, коли ви це робите.
svick

5

Я особисто думаю, що проблема, висвітлена в цій статті, пов'язана з надмірним використанням IEnumerableінтерфейсу багатьма розробниками C # в наші дні. Схоже, він став типом за замовчуванням, коли вам потрібна колекція об'єктів, але є багато інформації, яка IEnumerableпросто не каже вам ... головне: чи було вона перероблена чи ні *.

Якщо ви виявите, що вам потрібно виявити, чи IEnumerableсправді це IEnumerableчи щось інше, абстракція просочилася, і ви, мабуть, потрапили в ситуацію, коли тип вашого параметра занадто вільний. Якщо вам потрібна додаткова інформація, IEnumerableяка не надається, зробіть цю вимогу явною, вибравши один з інших інтерфейсів, таких як ICollectionабо IList.

Редагування для знижувачів. Поверніться та уважно прочитайте питання. ОП просить способу переконатися, що IEnumerableекземпляри були матеріалізовані перед тим, як перейти до його методу API. Моя відповідь стосується цього питання. Існує більш широке питання щодо правильного способу використання IEnumerable, і, безумовно, очікування коду, який було вставлено ОП, ґрунтується на нерозумінні цього більш широкого питання, але ОП не запитували, як правильно використовувати IEnumerableчи як працювати з незмінними типами . Мені не чесно, щоб я не відповідав на запитання, яке не було поставлене.

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


2
Одна з проблем , з ICollectionі в IListтому , що вони є змінними (або , по крайней мере , виглядати таким чином). IReadOnlyCollectionі IReadOnlyListз .Net 4.5 вирішують деякі з цих проблем.
svick

Поясніть, будь ласка, протокол?
MattDavey

1
Я не погоджуюся, що ви повинні змінити типи, які ви використовуєте. Численні - це чудовий тип, і він часто може принести користь незмінності. Я думаю, що справжня проблема полягає в тому, що люди не розуміють, як працювати з непорушними типами в C #. Не дивно, що це надзвичайно державна мова, тому це нова концепція для багатьох C # devs.
Джиммі Хоффа

Тільки безлічі дозволяють відкласти виконання, тому забороняючи його як параметр, видаляє одну цілу функцію C #
Джиммі Хоффа

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