Kembalikan semua enumerables dengan pengembalian hasil sekaligus; tanpa melalui perulangan

164

Saya memiliki fungsi berikut untuk mendapatkan kesalahan validasi kartu. Pertanyaan saya berkaitan dengan berurusan dengan GetErrors. Kedua metode memiliki tipe pengembalian yang sama 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
}

Apakah mungkin untuk mengembalikan semua kesalahan GetMoreErrorstanpa harus menyebutkannya?

Kalau dipikir-pikir ini mungkin pertanyaan bodoh, tapi aku ingin memastikan aku tidak salah.

John Oxley
sumber
Saya senang (dan ingin tahu!) Untuk melihat lebih banyak pertanyaan pengembalian hasil muncul - Saya sendiri tidak begitu memahaminya. Bukan pertanyaan bodoh!
JoshJordan
Apa GetCardProductionValidationErrorsFor?
Andrew Hare
4
apa yang salah dengan pengembalian GetMoreErrors (kartu); ?
Sam Saffron
10
@ Sam: "pengembalian hasil lebih lanjut untuk lebih banyak kesalahan validasi"
Jon Skeet
1
Dari sudut pandang bahasa yang tidak ambigu, satu masalah adalah bahwa metode ini tidak dapat mengetahui apakah ada sesuatu yang mengimplementasikan T dan IEnumerable <T>. Jadi Anda memerlukan konstruk berbeda dalam hasil. Yang mengatakan, pasti akan menyenangkan memiliki cara untuk melakukan ini. Imbal hasil hasil foo, mungkin, di mana foo mengimplementasikan IEnumerable <T>?
William Jockusch

Jawaban:

140

Ini jelas bukan pertanyaan bodoh, dan itu adalah sesuatu yang didukung F # yield!untuk seluruh koleksi vs yielduntuk satu item. (Itu bisa sangat berguna dalam hal rekursi ekor ...)

Sayangnya itu tidak didukung di C #.

Namun, jika Anda memiliki beberapa metode yang masing-masing mengembalikan IEnumerable<ErrorInfo>, Anda dapat menggunakan Enumerable.Concatuntuk membuat kode Anda lebih sederhana:

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

Ada satu perbedaan yang sangat penting antara kedua implementasi: yang satu ini akan memanggil semua metode segera , meskipun itu hanya akan menggunakan iterator yang dikembalikan satu per satu. Kode Anda yang ada akan menunggu sampai di-looped melalui semua yang ada di GetMoreErrors()dalamnya bahkan sebelum menanyakan tentang kesalahan selanjutnya.

Biasanya ini tidak penting, tetapi perlu memahami apa yang akan terjadi ketika.

Jon Skeet
sumber
3
Wes Dyer memiliki artikel menarik yang menyebutkan pola ini. blogs.msdn.com/wesdyer/archive/2007/03/23/…
JohannesH
1
Koreksi kecil untuk pelintas oleh - itu System.Linq.Enumeration.Compat <> (pertama, kedua). Bukan IEnumeration.Concat ().
redcalx
@ the-locster: Saya tidak yakin apa yang Anda maksud. Ini pasti Enumerable daripada Enumeration. Bisakah Anda mengklarifikasi komentar Anda?
Jon Skeet
@ Jon Skeet - Apa maksud Anda sebenarnya yang akan memanggil metode segera? Saya menjalankan tes dan sepertinya itu menunda panggilan metode sepenuhnya sampai sesuatu benar-benar diulang. Kode di sini: pastebin.com/0kj5QtfD
Steven Oxley
5
@ Seven: Tidak. Itu memanggil metode - tetapi dalam kasus Anda GetOtherErrors()(dll) menunda hasil mereka (karena mereka diimplementasikan menggunakan blok iterator). Coba ubah mereka untuk mengembalikan array baru atau sesuatu seperti itu, dan Anda akan melihat apa yang saya maksud.
Jon Skeet
26

Anda dapat mengatur semua sumber kesalahan seperti ini (nama metode dipinjam dari jawaban Jon Skeet).

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

Anda kemudian dapat mengulanginya secara bersamaan.

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

Atau Anda dapat meratakan sumber kesalahan dengan SelectMany.

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

Eksekusi metode di GetErrorSourcesakan ditunda juga.

Adam Boddington
sumber
16

Saya datang dengan yield_cuplikan singkat :

yield_ memotong animasi penggunaan

Berikut ini cuplikan 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>
John Gietzen
sumber
2
Bagaimana ini merupakan jawaban untuk pertanyaan itu?
Ian Kemp
@Ian, ini adalah bagaimana Anda harus melakukan pengembalian hasil bersarang di C #. Tidak ada yield!, seperti di F #.
John Gietzen
ini bukan jawaban untuk pertanyaan
divyang4481
8

Saya tidak melihat ada yang salah dengan fungsi Anda, saya akan mengatakan itu melakukan apa yang Anda inginkan.

Pikirkan Yield sebagai mengembalikan elemen dalam Enumerasi akhir setiap kali itu dipanggil, jadi ketika Anda memilikinya di loop foreach seperti itu, setiap kali itu dipanggil mengembalikan 1 elemen. Anda memiliki kemampuan untuk menempatkan pernyataan bersyarat di muka Anda untuk memfilter hasil. (hanya dengan tidak menghasilkan kriteria pengecualian Anda)

Jika Anda menambahkan hasil berikutnya nanti dalam metode ini, itu akan terus menambahkan 1 elemen ke enumerasi, sehingga memungkinkan untuk melakukan hal-hal seperti ...

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

Saya terkejut tidak ada yang berpikir untuk merekomendasikan metode Ekstensi sederhana IEnumerable<IEnumerable<T>>untuk membuat kode ini tetap ditangguhkan pelaksanaannya. Saya penggemar eksekusi yang ditangguhkan karena berbagai alasan, salah satunya adalah jejak memori yang kecil bahkan untuk enumerables yang sangat besar.

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

Dan Anda bisa menggunakannya dalam kasus Anda seperti ini

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
}

Demikian pula, Anda dapat menghilangkan fungsi pembungkus DoGetErrorsdan hanya pindah UnWrapke lokasi panggilan.

Frank Bryce
sumber
2
Mungkin tidak ada yang memikirkan metode Extension karena DoGetErrors(card).SelectMany(x => x)melakukan hal yang sama dan mempertahankan perilaku yang ditangguhkan. Persis seperti yang disarankan Adam dalam jawabannya .
huysentruitw
3

Ya, adalah mungkin untuk mengembalikan semua kesalahan sekaligus. Kembalikan a List<T>atau ReadOnlyCollection<T>.

Dengan mengembalikan IEnumerable<T>Anda mengembalikan urutan sesuatu. Pada permukaan yang mungkin terlihat identik dengan mengembalikan koleksi, tetapi ada sejumlah perbedaan, Anda harus ingat.

Koleksi

  • Penelepon dapat memastikan bahwa koleksi dan semua item akan ada ketika koleksi dikembalikan. Jika koleksi harus dibuat per panggilan, mengembalikan koleksi adalah ide yang sangat buruk.
  • Sebagian besar koleksi dapat dimodifikasi ketika dikembalikan.
  • Koleksi ini berukuran terbatas.

Urutan

  • Dapat disebutkan - dan itu cukup banyak yang bisa kita katakan dengan pasti.
  • Urutan yang dikembalikan itu sendiri tidak dapat dimodifikasi.
  • Setiap elemen dapat dibuat sebagai bagian dari menjalankan urutan (yaitu pengembalian IEnumerable<T>memungkinkan untuk evaluasi malas, kembali List<T>tidak).
  • Suatu urutan mungkin tidak terbatas dan dengan demikian menyerahkannya kepada pemanggil untuk memutuskan berapa banyak elemen yang harus dikembalikan.
Brian Rasmussen
sumber
Mengembalikan koleksi dapat menghasilkan overhead yang tidak masuk akal jika semua klien benar-benar perlu untuk menghitungnya, karena Anda mengalokasikan struktur data untuk semua elemen di muka. Juga, jika Anda mendelegasikan ke metode lain yang mengembalikan urutan, maka menangkapnya sebagai koleksi melibatkan penyalinan tambahan, dan Anda tidak tahu berapa banyak item (dan dengan demikian berapa banyak overhead) yang mungkin terlibat. Jadi, itu hanya ide yang baik untuk mengembalikan koleksi ketika sudah ada dan dapat dikembalikan langsung tanpa menyalin (atau dibungkus sebagai hanya baca). Dalam semua kasus lain, urutan adalah pilihan yang lebih baik
Pavel Minaev
Saya setuju, dan jika Anda mendapat kesan bahwa saya mengatakan mengembalikan koleksi selalu merupakan ide bagus, Anda melewatkan poin saya. Saya mencoba menyoroti fakta bahwa ada perbedaan antara mengembalikan koleksi dan mengembalikan urutan. Saya akan mencoba membuatnya lebih jelas.
Brian Rasmussen