Wykrywanie niezliczonych „automatów stanowych”

17

Właśnie przeczytałem interesujący artykuł zatytułowany Robi się zbyt słodki z zyskiem c #

Zastanawiałem się, jaki jest najlepszy sposób na wykrycie, czy IEnumerable jest faktyczną kolekcją, którą można wyliczyć, czy też jest maszyną stanu wygenerowaną za pomocą słowa kluczowego fed.

Na przykład możesz zmodyfikować DoubleXValue (z artykułu) na coś takiego:

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

Pytanie 1) Czy jest na to lepszy sposób?

Pytanie 2) Czy powinienem się tym martwić podczas tworzenia interfejsu API?

ConditionRacer
źródło
1
Powinieneś użyć interfejsu zamiast konkretnego, a także rzucić niżej, ICollection<T>byłby lepszym wyborem, ponieważ nie wszystkie kolekcje są List<T>. Na przykład tablice Point[]implementują, IList<T>ale nie są List<T>.
Trevor Pilley,
3
Witamy w problemie ze zmiennymi typami. Ienumerable ma domyślnie być niezmiennym typem danych, więc mutacje implementacyjne stanowią naruszenie LSP. Ten artykuł jest doskonałym wyjaśnieniem zagrożeń związanych z efektami ubocznymi, więc poćwicz pisanie typów C #, aby być jak najbardziej wolnym od efektów ubocznych i nigdy nie musisz się o to martwić.
Jimmy Hoffa
1
@JimmyHoffa IEnumerablejest niezmienny w tym sensie, że nie można modyfikować kolekcji (dodawać / usuwać) podczas jej wyliczania, jednak jeśli obiekty na liście można modyfikować, to całkiem rozsądne jest zmodyfikowanie ich podczas wyliczania listy.
Trevor Pilley
@TrevorPilley Nie zgadzam się. Jeśli oczekuje się, że kolekcja będzie niezmienna, od konsumenta oczekuje się, że członkowie nie zostaną zmutowani przez podmioty zewnętrzne, co byłoby nazywane efektem ubocznym i narusza oczekiwania dotyczące niezmienności. Narusza POLA i pośrednio LSP.
Jimmy Hoffa
Co powiesz na używanie ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Nie wykrywa, czy został wygenerowany przez wydajność, ale czyni go nieistotnym.
luiscubal

Odpowiedzi:

22

Twoje pytanie, jak rozumiem, wydaje się opierać na niewłaściwym założeniu. Zobaczmy, czy mogę zrekonstruować rozumowanie:

  • Artykuł z linkami opisuje, w jaki sposób automatycznie generowane sekwencje wykazują zachowanie „leniwe” i pokazuje, jak może to prowadzić do sprzecznych z intuicją wyników.
  • Dlatego mogę wykryć, czy dana instancja IEnumerable będzie wykazywać to leniwe zachowanie, sprawdzając, czy jest ona generowana automatycznie.
  • Jak mogę to zrobić?

Problem polega na tym, że druga przesłanka jest fałszywa. Nawet gdybyś mógł wykryć, czy dany IEnumerable był wynikiem transformacji bloku iteratora (i tak, istnieją sposoby, aby to zrobić), nie pomogłoby to, ponieważ założenie jest błędne. Zilustrujmy dlaczego.

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;
  }
}

W porządku, mamy cztery metody. S1 i S2 są automatycznie generowanymi sekwencjami; S3 i S4 są sekwencjami generowanymi ręcznie. Załóżmy teraz, że mamy:

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

Wynik dla S1 i S4 wyniesie 0; za każdym razem, gdy wyliczasz sekwencję, dostajesz nowe odniesienie do utworzonego M. Wynik dla S2 i S3 wyniesie 100; za każdym razem, gdy wyliczasz sekwencję, dostajesz to samo odniesienie do M, które dostałeś ostatni raz. To, czy kod sekwencji jest generowany automatycznie, czy nie, jest prostopadłe do pytania, czy wyliczone obiekty mają tożsamość referencyjną, czy nie. Te dwie właściwości - automatyczne generowanie i tożsamość referencyjna - w rzeczywistości nie mają ze sobą nic wspólnego. Artykuł, który podlinkowałeś, nieco je łączy.

O ile dostawca sekwencji nie jest udokumentowany jako zawsze oferujący obiekty, które mają tożsamość referencyjną , nie należy zakładać, że tak jest.

Eric Lippert
źródło
2
Wszyscy wiemy, że masz tutaj poprawną odpowiedź, to jest dane; ale jeśli możesz podać nieco więcej definicji terminu „tożsamość referencyjna”, może to być dobre, czytam to jako „niezmienne”, ale dzieje się tak dlatego, że łączę niezmienny w języku C # z nowym obiektem zwracającym każdy typ get lub value, co myślę to ta sama rzecz, o której mówisz. Chociaż przyznaną niezmienność można uzyskać na wiele innych sposobów, jest to jedyny racjonalny sposób w języku C # (jestem tego świadomy, być może znasz sposoby, których nie jestem świadomy).
Jimmy Hoffa
2
@JimmyHoffa: Dwa odwołania do obiektów xiy mają „tożsamość referencyjną”, jeśli Object.ReferenceEquals (x, y) zwraca true.
Eric Lippert,
Zawsze miło jest uzyskać odpowiedź bezpośrednio ze źródła. Dzięki Eric.
ConditionRacer
10

Myślę, że kluczową koncepcją jest to, że każdą IEnumerable<T>kolekcję należy traktować jako niezmienną : nie należy modyfikować z niej obiektów, należy utworzyć nową kolekcję z nowymi obiektami:

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

(Lub napisz to bardziej zwięźle, używając LINQ Select().)

Jeśli naprawdę chcesz zmodyfikować elementy w kolekcji, nie używaj IEnumerable<T>, nie używaj IList<T>(lub ewentualnie IReadOnlyList<T>jeśli nie chcesz modyfikować samej kolekcji, tylko elementy w niej, a jesteś w .Net 4.5):

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

W ten sposób, jeśli ktoś spróbuje użyć twojej metody IEnumerable<T>, zawiedzie ona podczas kompilacji.

svick
źródło
1
Prawdziwy klucz jest taki, jak powiedziałeś, wróć nowy niezliczony z modyfikacjami, gdy chcesz coś zmienić. Niezliczony jako typ jest całkowicie wykonalny i nie ma powodu, aby go zmieniać, to tylko technika jego używania musi być zrozumiana, jak powiedziałeś. +1 za wspomnienie o pracy z niezmiennymi typami.
Jimmy Hoffa
1
Nieco spokrewnione - każdy IEnumerable należy traktować tak, jakby to było zrobione przez odroczenie wykonania. To, co może być prawdą w momencie pobierania referencji IEnumerable, może nie być prawdziwe, gdy faktycznie ją wyliczasz. Na przykład, zwracanie instrukcji LINQ wewnątrz zamka może spowodować interesujące nieoczekiwane wyniki.
Dan Lyons,
@JimmmyHoffa: Zgadzam się. Idę o krok dalej i staram się unikać IEnumerablewielokrotnego powtarzania . Potrzeba zrobienia tego więcej niż jeden raz można uniknąć, wywołując ToList()i iterując listę, chociaż w konkretnym przypadku pokazanym przez PO wolę rozwiązanie svicka polegające na tym, że DoubleXValue również zwraca IEnumerable. Oczywiście w prawdziwym kodzie prawdopodobnie LINQużyłbym tego do wygenerowania tego typu IEnumerable. Jednak wybór Svicka yieldma sens w kontekście pytania PO.
Brian
@Brian Tak, nie chciałem zbytnio zmieniać kodu, aby o tym wspomnieć. W prawdziwym kodzie użyłbym Select(). A jeśli chodzi o wielokrotne iteracje IEnumerable: ReSharper jest w tym pomocny, ponieważ może cię ostrzec, kiedy to zrobisz.
sick
5

Osobiście uważam, że problem wskazany w tym artykule wynika z nadmiernego używania IEnumerableinterfejsu przez wielu programistów C # w dzisiejszych czasach. Wygląda na to, że stał się domyślnym typem, gdy potrzebujesz kolekcji obiektów - ale jest wiele informacji, które IEnumerablepo prostu nie mówią ci ... co najważniejsze: czy zostało to potwierdzone czy nie *.

Jeśli okaże się, że musisz wykryć, czy an IEnumerablejest naprawdę an, IEnumerableczy czymś innym, abstrakcja wyciekła i prawdopodobnie jesteś w sytuacji, gdy typ parametru jest nieco zbyt luźny. Jeśli potrzebujesz dodatkowych informacji, których IEnumerablesam nie zapewnia, wyraź to wymaganie, wybierając jeden z innych interfejsów, takich jak ICollectionlub IList.

Edytuj dla downvoters Wróć i przeczytaj uważnie pytanie. OP prosi o sposób zagwarantowania, że IEnumerableinstancje zostały zmaterializowane przed przekazaniem ich do jego metody API. Moja odpowiedź dotyczy tego pytania. Istnieje szerszy problem dotyczący właściwego sposobu użycia IEnumerable, a na pewno oczekiwania dotyczące kodu, który wklejono OP, są oparte na nieporozumieniu tego szerszego problemu, ale OP nie zapytał, jak prawidłowo używać IEnumerablelub jak pracować z niezmiennymi typami . Nie jest sprawiedliwe głosować za to, że nie odpowiedziałem na pytanie, które nie zostało postawione.

* Używam tego terminu reify, inni używają stabilizelub materialize. Nie sądzę, aby społeczność przyjęła jeszcze wspólną konwencję.

MattDavey
źródło
2
Jednym z problemów ICollection, a IListjest to, że są one zmienne (lub przynajmniej patrzeć w ten sposób). IReadOnlyCollectioni IReadOnlyList.Net 4.5 rozwiązać niektóre z tych problemów.
sick
Proszę wyjaśnić opinię?
MattDavey
1
Nie zgadzam się, że powinieneś zmienić typy, których używasz. Niezliczona jest świetnym typem i często może mieć zaletę niezmienności. Myślę, że prawdziwym problemem jest to, że ludzie nie rozumieją, jak pracować z niezmiennymi typami w C #. Nic dziwnego, że jest to niezwykle sprawny język, więc jest to nowa koncepcja dla wielu programistów C #.
Jimmy Hoffa
Tylko niezliczona pozwala na odroczenie wykonania, więc niedopuszczenie jej jako parametru usuwa jedną całą funkcję C #
Jimmy Hoffa
2
Głosowałem za tym, ponieważ uważam, że to źle. Myślę, że jest to w duchu SE, aby zapewnić, że ludzie nie będą podawać nieprawidłowych informacji, od nas wszystkich zależy głosowanie za odrzuceniem tych odpowiedzi. Może się mylę, całkowicie możliwe, że się mylę, a ty masz rację. To do szerszej społeczności decyduje głosowanie. Przepraszam, że bierzesz to osobiście, jeśli to jakaś pociecha, miałem wiele negatywnych, głosowanych odpowiedzi, które napisałem i streściłem, ponieważ akceptuję społeczność, która uważa, że ​​to źle, więc raczej nie mam racji.
Jimmy Hoffa