Mendeteksi IEnumerable "Mesin Negara"

17

Saya baru saja membaca artikel yang menarik berjudul Getting too cute with c # yield return

Itu membuat saya bertanya-tanya apa cara terbaik untuk mendeteksi apakah IEnumerable adalah kumpulan enumerable yang sebenarnya, atau apakah itu mesin negara yang dihasilkan dengan kata kunci hasil.

Misalnya, Anda bisa memodifikasi DoubleXValue (dari artikel) menjadi sesuatu seperti:

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

Pertanyaan 1) Apakah ada cara yang lebih baik untuk melakukan ini?

Pertanyaan 2) Apakah ini sesuatu yang harus saya khawatirkan ketika membuat API?

ConditionRacer
sumber
1
Anda harus menggunakan antarmuka daripada yang konkret dan juga menurunkan lebih rendah, ICollection<T>akan menjadi pilihan yang lebih baik karena tidak semua koleksi List<T>. Misalnya, array Point[]implementIList<T> tetapi tidak List<T>.
Trevor Pilley
3
Selamat datang di masalah dengan tipe yang bisa berubah. Ienumerable secara implisit dianggap sebagai tipe data yang tidak dapat diubah, sehingga implementasi yang bermutasi merupakan pelanggaran terhadap LSP. Artikel ini adalah penjelasan sempurna tentang bahaya efek samping, jadi berlatih menulis tipe C # Anda sebagai efek samping sebisa mungkin dan Anda tidak perlu khawatir tentang hal ini.
Jimmy Hoffa
1
@JimmyHoffa IEnumerabletidak dapat diubah dalam arti bahwa Anda tidak dapat memodifikasi koleksi (menambah / menghapus) ketika menghitungnya, namun jika objek dalam daftar dapat berubah, sangat masuk akal untuk memodifikasi mereka saat menghitung daftar.
Trevor Pilley
@ TrevorPilley saya tidak setuju. Jika koleksi diharapkan tidak berubah, harapan dari konsumen adalah bahwa anggota tidak akan dimutasi oleh aktor luar, yang akan disebut efek samping dan melanggar ekspektasi keabadian. Melanggar POLA dan secara implisit LSP.
Jimmy Hoffa
Bagaimana kalau menggunakan ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Itu tidak mendeteksi apakah itu dihasilkan oleh hasil, tetapi malah membuatnya tidak relevan.
Luiscubal

Jawaban:

22

Pertanyaan Anda, seperti yang saya mengerti, tampaknya didasarkan pada premis yang salah. Biarkan saya melihat apakah saya dapat merekonstruksi alasannya:

  • Artikel yang ditautkan ke menjelaskan bagaimana urutan yang dibuat secara otomatis menunjukkan perilaku "malas", dan menunjukkan bagaimana ini dapat mengarah pada hasil kontra-intuitif.
  • Oleh karena itu saya dapat mendeteksi apakah contoh IEnumerable yang diberikan akan menunjukkan perilaku malas ini dengan memeriksa untuk melihat apakah itu dihasilkan secara otomatis.
  • Bagaimana aku melakukan itu?

Masalahnya adalah premis kedua salah. Bahkan jika Anda dapat mendeteksi apakah IEnumerable yang diberikan adalah hasil dari transformasi blok iterator (dan ya, ada cara untuk melakukan itu) tidak akan membantu karena anggapan tersebut salah. Mari kita ilustrasikan alasannya.

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

Baiklah, kita punya empat metode. S1 dan S2 adalah urutan yang dihasilkan secara otomatis; S3 dan S4 adalah urutan yang dibuat secara manual. Sekarang misalkan kita memiliki:

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

Hasil untuk S1 dan S4 akan menjadi 0; setiap kali Anda menyebutkan urutannya, Anda mendapatkan referensi baru ke huruf M yang dibuat. Hasil untuk S2 dan S3 adalah 100; setiap kali Anda menyebutkan urutannya, Anda mendapatkan referensi yang sama dengan M yang Anda dapatkan terakhir kali.Apakah kode urutan dihasilkan secara otomatis atau tidak adalah ortogonal terhadap pertanyaan apakah objek yang disebutkan memiliki identitas referensial atau tidak. Dua properti itu - pembangkitan otomatis dan identitas referensial - sebenarnya tidak ada hubungannya satu sama lain. Artikel yang Anda tautkan agak mengkonfigurasinya.

Kecuali penyedia urutan didokumentasikan sebagai selalu menawarkan benda-benda yang memiliki identitas referensial , tidak bijaksana untuk berasumsi bahwa ia melakukannya.

Eric Lippert
sumber
2
Kita semua tahu Anda akan memiliki jawaban yang benar di sini, itu sudah pasti; tetapi jika Anda bisa memasukkan sedikit definisi ke dalam istilah "identitas referensial" itu mungkin baik, saya membacanya sebagai "tidak dapat diubah" tetapi itu karena saya mengonversikan kekal dalam C # dengan objek baru yang dikembalikan setiap get atau tipe nilai, yang menurut saya adalah hal yang sama yang Anda maksud. Meskipun kekekalan yang diberikan dapat dimiliki dalam berbagai cara lain, itu satu-satunya cara rasional dalam C # (yang saya sadari, Anda mungkin tahu cara-cara yang tidak saya sadari).
Jimmy Hoffa
2
@JimmyHoffa: Dua referensi objek x dan y memiliki "identitas referensial" jika Object.ReferenceEquals (x, y) mengembalikan true.
Eric Lippert
Selalu menyenangkan mendapat tanggapan langsung dari sumbernya. Eric terima kasih.
ConditionRacer
10

Saya pikir konsep kuncinya adalah bahwa Anda harus memperlakukan IEnumerable<T>koleksi apa pun sebagai tidak berubah : Anda tidak boleh memodifikasi objek dari itu, Anda harus membuat koleksi baru dengan objek baru:

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

(Atau tulis yang sama lebih ringkas menggunakan LINQ Select().)

Jika Anda benar-benar ingin memodifikasi item dalam koleksi, jangan gunakan IEnumerable<T>, gunakan IList<T>(atau mungkin IReadOnlyList<T>jika Anda tidak ingin mengubah koleksi itu sendiri, hanya item di dalamnya, dan Anda menggunakan .Net 4.5):

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

Dengan cara ini, jika seseorang mencoba menggunakan metode Anda IEnumerable<T>, itu akan gagal pada waktu kompilasi.

svick
sumber
1
Kunci sebenarnya adalah seperti yang Anda katakan, mengembalikan ienumerable baru dengan modifikasi ketika Anda ingin mengubah sesuatu. Dapat dihitung sebagai suatu jenis yang sangat layak dan tidak memiliki alasan untuk diubah, hanya saja teknik untuk menggunakannya perlu dipahami seperti yang Anda katakan. +1 untuk menyebutkan cara bekerja dengan tipe yang tidak bisa diubah.
Jimmy Hoffa
1
Agak terkait - Anda juga harus memperlakukan setiap IEnumerable seolah-olah dilakukan melalui eksekusi yang ditangguhkan. Apa yang mungkin benar pada saat Anda mengambil referensi IEnumerable Anda mungkin tidak benar ketika Anda benar-benar menghitungnya. Misalnya, mengembalikan pernyataan LINQ di dalam kunci dapat menghasilkan beberapa hasil yang menarik dan tidak terduga.
Dan Lyons
@JimmyHoffa: Saya setuju. Saya melangkah lebih jauh dan mencoba untuk menghindari iterasi IEnumerablelebih dari sekali. Kebutuhan untuk melakukannya lebih dari sekali dapat dihilangkan dengan menelepon ToList()dan mengulangi daftar, meskipun dalam kasus khusus yang ditunjukkan oleh OP saya lebih suka solusi svick memiliki DoubleXValue juga mengembalikan IEnumerable. Tentu saja, dalam kode nyata saya mungkin akan menggunakan LINQuntuk menghasilkan jenis ini IEnumerable. Namun, pilihan svick yieldmasuk akal dalam konteks pertanyaan OP.
Brian
@ Brian Ya, saya tidak ingin mengubah kode terlalu banyak untuk menjelaskan maksud saya. Dalam kode nyata, saya akan menggunakan Select(). Dan mengenai beberapa iterasi dari IEnumerable: ReSharper sangat membantu dengan itu, karena itu dapat memperingatkan Anda ketika Anda melakukan itu.
svick
5

Saya pribadi berpikir masalah yang disoroti dalam artikel itu berasal dari terlalu sering menggunakan IEnumerableantarmuka oleh banyak pengembang C # hari ini. Tampaknya telah menjadi tipe default ketika Anda membutuhkan koleksi objek - tetapi ada banyak informasi yang IEnumerabletidak memberi tahu Anda ... yang terpenting: apakah sudah diverifikasi * atau tidak.

Jika Anda menemukan bahwa Anda perlu mendeteksi apakah IEnumerablebenar-benar sebuah IEnumerableatau sesuatu yang lain, abstraksi telah bocor dan Anda mungkin dalam situasi di mana tipe parameter Anda agak terlalu longgar. Jika Anda memerlukan informasi tambahan yang IEnumerabletidak disediakan sendiri, buat persyaratan itu eksplisit dengan memilih salah satu antarmuka lain seperti ICollectionatau IList.

Edit untuk downvoters Harap kembali dan baca pertanyaan dengan seksama. OP meminta cara untuk memastikan bahwa IEnumerableinstance telah terwujud sebelum diteruskan ke metode API-nya. Jawaban saya menjawab pertanyaan itu. Ada masalah yang lebih luas tentang cara penggunaan yang benarIEnumerable , dan tentu saja harapan kode yang ditempelkan OP didasarkan pada kesalahpahaman tentang masalah yang lebih luas itu, tetapi OP tidak bertanya bagaimana menggunakan dengan benar IEnumerable, atau bagaimana bekerja dengan jenis yang tidak dapat diubah . Tidak adil untuk menurunkan saya karena tidak menjawab pertanyaan yang tidak diajukan.

* Saya menggunakan istilah reify, yang lain menggunakan stabilizeatau materialize. Saya tidak berpikir konvensi umum telah diadopsi oleh komunitas dulu.

MattDavey
sumber
2
Satu masalah dengan ICollectiondan IListadalah bahwa mereka bisa berubah (atau setidaknya terlihat seperti itu). IReadOnlyCollectiondan IReadOnlyListdari .Net 4.5 memecahkan beberapa masalah tersebut.
svick
Tolong jelaskan downvote?
MattDavey
1
Saya tidak setuju bahwa Anda harus mengubah jenis yang Anda gunakan. Ienumerable adalah tipe yang hebat dan sering kali dapat memiliki manfaat kekekalan. Saya pikir masalah sebenarnya adalah orang tidak mengerti bagaimana bekerja dengan tipe yang tidak dapat diubah dalam C #. Tidak mengherankan, ini adalah bahasa yang sangat stateful, jadi ini adalah konsep baru bagi banyak C # devs.
Jimmy Hoffa
Hanya ienumerable yang memungkinkan eksekusi yang ditangguhkan, jadi melarangnya sebagai parameter menghapus satu fitur C #
Jimmy Hoffa
2
Saya memilih karena saya pikir itu salah. Saya pikir itu adalah semangat SE, untuk memastikan orang tidak memasukkan informasi yang salah, terserah kita semua untuk memilih suara jawaban itu. Mungkin aku salah, sangat mungkin aku salah dan kau benar. Terserah komunitas yang lebih luas untuk memutuskan dengan memilih. Maaf Anda menganggapnya pribadi, jika itu adalah penghiburan, saya memiliki banyak jawaban negatif yang telah saya tulis dan ringkasnya dihapus karena saya menerima komunitas berpikir itu salah jadi saya tidak mungkin benar.
Jimmy Hoffa