IEnumerable “Durum Makinelerini” Tespit Etme


17

Ben sadece c # verim dönüşü ile çok şirin Başlarken adlı ilginç bir makale okudum

Bir IEnumerable'ın gerçek bir numaralandırılabilir koleksiyon olup olmadığını veya verim anahtar kelimesi ile oluşturulan bir durum makinesi olup olmadığını tespit etmenin en iyi yolunun ne olduğunu merak etmemi sağladı.

Örneğin, DoubleXValue'yu (makaleden) aşağıdaki gibi değiştirebilirsiniz:

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

Soru 1) Bunu yapmanın daha iyi bir yolu var mı?

Soru 2) Bu bir API oluştururken endişelenmem gereken bir şey mi?


1
Betondan ziyade bir arayüz kullanmalı ve ayrıca aşağı dökülmeli, ICollection<T>tüm koleksiyonlar olmadığı için daha iyi bir seçim olacaktır List<T>. Örneğin, diziler Point[]uygulanır IList<T>ancak uygulanmaz List<T>.
Trevor Pilley

3
Değişken tiplerle ilgili soruna hoş geldiniz. Yinelenebilir, dolaylı olarak değişmez bir veri tipi olduğu varsayılır, bu nedenle mutasyona uğratıcı uygulamalar LSP'yi ihlal eder. Bu makale yan etkilerin tehlikelerinin mükemmel bir açıklamasıdır, bu yüzden C # tiplerinizi mümkün olduğunca ücretsiz yan etki olarak yazmayı deneyin ve bu konuda asla endişelenmenize gerek yoktur.
Jimmy Hoffa

1
@JimmyHoffa IEnumerable, bir koleksiyonu numaralandırırken değiştiremeyeceğiniz (ekleyemez / kaldıramazsınız) anlamında değişmezdir, ancak listedeki nesneler değiştirilebilirse, listeyi numaralandırırken bunları değiştirmek son derece mantıklıdır.
Trevor Pilley

@TrevorPilley Kabul etmiyorum. Koleksiyonun değişmez olması bekleniyorsa, bir tüketiciden beklentiler, üyelerin dış aktörler tarafından mutasyona uğramayacağı, bunun bir yan etki olarak adlandırılacağı ve değişmezlik beklentisini ihlal edeceğidir. POLA'yı ve dolaylı olarak LSP'yi ihlal eder.
Jimmy Hoffa

Kullanmaya ne dersiniz ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Verim tarafından üretilip üretilmediğini algılamaz, bunun yerine ilgisiz kılar.
luiscubal

Yanıtlar:


22

Sorunuz, anladığım kadarıyla, yanlış bir önermeye dayanıyor gibi görünüyor. Bakalım akıl yürütmeyi yeniden yapılandırabilir miyim:

  • Bağlantılı makale, otomatik olarak oluşturulan sekansların nasıl "tembel" bir davranış sergilediğini ve bunun nasıl sezgisel bir sonuca yol açabileceğini gösterir.
  • Bu nedenle, belirli bir IEnumerable örneğinin otomatik olarak oluşturulup oluşturulmadığını kontrol ederek bu tembel davranışı gösterip göstermeyeceğini tespit edebilirim.
  • Bunu nasıl yaparım?

Sorun, ikinci öncülün yanlış olmasıdır. Belirli bir IEnumerable'ın bir yineleyici blok dönüşümünün sonucu olup olmadığını tespit edebilseniz bile (ve evet, bunu yapmanın yolları vardır) varsayım yanlış olduğu için yardımcı olmaz. Nedenini açıklayalım.

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

Pekala, dört yöntemimiz var. S1 ve S2 otomatik olarak oluşturulan dizilerdir; S3 ve S4, manuel olarak oluşturulan dizilerdir. Şimdi varsayalım:

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

S1 ve S4 için sonuç 0 olacaktır; diziyi her numaralandırdığınızda, oluşturulan bir M'ye yeni bir referans alırsınız. S2 ve S3 için sonuç 100 olacaktır; diziyi her numaralandırdığınızda, M'ye aynı referansı son kez alırsınız. Sıralama kodunun otomatik olarak oluşturulup oluşturulmadığı, numaralandırılan nesnelerin referans kimliğine sahip olup olmadığı sorusuyla dikeydir. Bu iki özelliğin - otomatik üretim ve referans kimlik - aslında birbirleriyle hiçbir ilgisi yoktur. Bağlantı verdiğiniz makale onları biraz karıştırıyor.

Bir sekans sağlayıcısı, her zaman referans kimliğine sahip nesneleri önermiş olarak belgelenmedikçe , bunu kabul ettiği akılsızdır.


2
Burada doğru cevaba sahip olacağınızı hepimiz biliyoruz, bu bir veridir; ama "referans kimlik" terimine biraz daha fazla tanım koyabilirseniz iyi olabilir, bunu "değişmez" olarak okuyorum ama bunun sebebi C # 'da değişmez yeni nesne ile her get veya değer türünü döndürdüğünü düşünüyorum. bahsettiğiniz şeyle aynı. Verilen değişmezlik çeşitli başka yollarla elde edilebilir olsa da, C # 'daki tek rasyonel yol (farkında olduğum, farkında olmadığım yolları iyi biliyor olabilirsiniz).
Ocak'ta Jimmy Hoffa

2
@JimmyHoffa: Object.ReferenceEquals (x, y) true değerini döndürürse, x ve y adlı iki nesne başvurusu "başvuru kimliğine" sahiptir.
Eric Lippert

Doğrudan kaynaktan yanıt almak her zaman güzeldir. Teşekkürler Eric.
ConditionRacer

10

Bence anahtar kavram, herhangi bir IEnumerable<T>koleksiyonu değişmez olarak ele almanız gerektiğidir: ondan nesneleri değiştirmemelisiniz, yeni nesnelerle yeni bir koleksiyon oluşturmalısınız:

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

(Ya da aynısını LINQ kullanarak daha özlü bir şekilde yazın Select().)

Koleksiyondaki öğeleri gerçekten değiştirmek istiyorsanız, kullanmayın IEnumerable<T>, kullanın IList<T>(veya IReadOnlyList<T>koleksiyonun kendisini, yalnızca içindeki öğeleri değiştirmek istemiyorsanız ve .Net 4.5'deyseniz):

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

Bu şekilde, birisi yönteminizi kullanmaya çalışırsa IEnumerable<T>derleme sırasında başarısız olur.


1
Gerçek anahtar, dediğin gibi, bir şeyi değiştirmek istediğinde değişikliklerle yeni bir ienumerable döndür. Bir tür olarak ölçülebilir, mükemmel bir şekilde uygulanabilir ve değiştirilmesi için bir nedeni yoktur, sadece bunu kullanmak için kullanılan teknik, dediğin gibi anlaşılmalıdır. Değişmez tiplerle nasıl çalışılacağından bahsettiğiniz için +1.
Ocak'ta Jimmy Hoffa

1
Biraz ilgili - her IEnumerable'a ertelenmiş yürütme ile yapılmış gibi davranmalısınız. IEnumerable referansınızı aldığınızda doğru olabilecek şeyler, gerçekte numaralandırıldığında doğru olmayabilir. Örneğin, bir kilit içinde bir LINQ deyimi döndürmek bazı ilginç beklenmedik sonuçlara neden olabilir.
Dan Lyons

@JimmyHoffa: Katılıyorum. Bir adım daha ileri gidip bir IEnumerablekereden fazla yinelemekten kaçınmaya çalışıyorum . ToList()OP tarafından gösterilen özel durumda, svick'in DoubleXValue'ya bir IEnumerable döndürme çözümünü de tercih etsem de, listeyi arayarak ve yineleyerek bir kereden fazla yapma ihtiyacı ortadan kaldırılabilir . Tabii ki, gerçek kodda muhtemelen LINQbu tür üretmek için kullanıyor olurdu IEnumerable. Bununla birlikte, svick'in yieldOP tercihi bağlamında mantıklı seçimi mantıklıdır.
Brian

@Brian Evet, kodumu çok fazla değiştirmek istemedim. Gerçek kod, ben olurdu Select(). Ve birden fazla yineleme ile ilgili olarak IEnumerable: ReSharper bu konuda yardımcı olur, çünkü bunu yaptığınızda sizi uyarabilir.
svick

5

Ben şahsen bu makalede vurgulanan sorunun IEnumerablebugünlerde birçok C # geliştiricileri tarafından arayüzü aşırı kullanımdan kaynaklandığını düşünüyorum . Nesnelerin bir koleksiyonuna ihtiyaç duyduğunuzda varsayılan tür haline gelmiş gibi görünüyor - ancak IEnumerablesize söylemeyen çok fazla bilgi var ... çok önemli: yeniden yapılandırılmış olsun ya da olmasın *.

An'ın gerçekten bir veya başka bir şey olup olmadığını tespit etmeniz gerektiğini fark ederseniz , soyutlama sızdı ve muhtemelen parametre türünüzün biraz fazla gevşek olduğu bir durumdasınız. Tek başına sağlanmayan ek bilgilere ihtiyacınız varsa veya gibi diğer arabirimlerden birini seçerek bu gereksinimi açıkça belirtin .IEnumerableIEnumerableIEnumerableICollectionIList

Downvoters için düzenleyin Lütfen geri dönün ve soruyu dikkatlice okuyun. OP, IEnumerableAPI yöntemine geçirilmeden önce örneklerin gerçekleştirildiğinden emin olmak için bir yol istemektedir . Cevabım bu soruyu ele alıyor. Doğru kullanım yolu ile ilgili daha geniş bir sorun var IEnumerableve kesinlikle OP'nin yapıştırdığı kodun beklentileri bu daha geniş sorunun yanlış anlaşılmasına dayanıyor, ancak OP doğru şekilde nasıl kullanılacağını IEnumerableveya değişmez türlerle nasıl çalışılacağını sormadı . Sorulmamış bir soruyu cevaplamadığım için beni aşağılamak adil değil.

* Terimini kullanıyorum reify, diğerleri stabilizeveya materialize. Topluluk tarafından henüz ortak bir konvansiyonun kabul edildiğini sanmıyorum.


2
Bir sorun ICollectionve IListonlar kesilebilir (veya en azından göz yolu o) olmasıdır. IReadOnlyCollectionve IReadOnlyList.NET 4.5 gelenler sorunlardan bazılarını çözmek.
svick

Lütfen downvote açıklamak?
MattDavey

1
Kullandığınız türleri değiştirmeniz gerektiğini kabul etmiyorum. Genişletilebilir harika bir tiptir ve genellikle değişmezlik avantajına sahip olabilir. Bence asıl sorun, insanların C # 'daki değişmez tiplerle nasıl çalışacaklarını anlamamasıdır. Şaşırtıcı değil, son derece durumlu bir dil, bu yüzden birçok C # geliştirici için yeni bir kavram.
Jimmy Hoffa

Sadece ienumerable ertelenmiş yürütmeye izin verir, bu yüzden bir parametre olarak izinsiz bırakmak C # bir bütün özelliğini kaldırır
Jimmy Hoffa

2
Oy verdim çünkü yanlış olduğunu düşünüyorum. Bence SE ruhu içinde, insanların yanlış bilgiye stok koymamalarını sağlamak, bu cevapları aşağıya çekmek hepimizin sorumluluğundadır. Belki yanılıyorum, yanılıyorum ve haklısın. Oy vererek karar vermek daha geniş bir topluluğa bağlıdır. Özür dilerim, kişisel olarak kabul ediyorsunuz, eğer herhangi bir teselli varsa, yazdığım ve özet olarak sildiğim birçok olumsuz oylanmış cevap aldım çünkü topluluğun yanlış olduğunu düşündüğünü kabul ediyorum, bu yüzden doğru değilim.
Ocak'ta Jimmy Hoffa
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.