Yineleyicinin yıkıcı olmayan zımni bir sözleşmesi var mı?


11

Diyelim ki bir yığın veya kuyruk gibi özel bir veri yapısı tasarlıyorum (örneğin - mantıksal eşdeğeri pushve popyöntemleri - yani yıkıcı erişimci yöntemleri) başka bir rasgele sıralı koleksiyon olabilir .

IEnumerable<T>Her bir yinelemede ortaya çıkan bu koleksiyon üzerinde bir yineleyici (özellikle .NET'te ) uyguluyorsanız, bu IEnumerable<T>zımni sözleşmeyi bozar mıydı?

Does IEnumerable<T>bu zımni sözleşmem var?

Örneğin:

public IEnumerator<T> GetEnumerator()
{
    if (this.list.Count > 0)
        yield return this.Pop();
    else
        yield break;
}

Yanıtlar:


12

Yıkıcı bir Sayıcı'nın En Az Şaşkınlık İlkesini ihlal ettiğine inanıyorum . Bir örnek olarak, genel rahatlık işlevleri sunan bir iş nesnesi kütüphanesi hayal edin. Masumca bir iş nesneleri koleksiyonu güncelleyen bir işlev yazın:

static void UpdateStatus<T>(IEnumerable<T> collection) where T : IBusinessObject
{
    foreach (var item in collection)
    {
        item.Status = BusinessObjectStatus.Foo;
    }
}

Uygulamam veya uygulamanız hakkında hiçbir şey bilmeyen başka bir geliştirici, her ikisini de masumca kullanmaya karar verir. Muhtemelen sonuçtan şaşıracaklar:

//developer uses your type without thinking about the details
var collection = GetYourEnumerableType<SomeBusinessObject>();

//developer uses my function without thinking about the details
UpdateStatus<SomeBusinessObject>(collection);

Geliştirici yıkıcı numaralandırmanın farkında olsa bile, koleksiyonu bir kara kutu işlevine teslim ederken yansımaları düşünmeyebilir. Yazarı olarak UpdateStatus, muhtemelen tasarımımda yıkıcı numaralandırmayı düşünmeyeceğim.

Ancak, bu sadece zımni bir sözleşmedir. .NET koleksiyonları da dahil olmak üzere Stack<T>, InvalidOperationException- "Numaralandırıcı başlatıldıktan sonra koleksiyon değiştirildi" ile açık bir sözleşme uygulamak . Gerçek bir profesyonelin, kendilerine ait olmayan herhangi bir koda karşı uyarıcı bir emptor tutumuna sahip olduğunu iddia edebilirsiniz . Yıkıcı bir numaralandırıcının sürprizleri minimal testlerle keşfedilecekti.


1
+1 - 'En Az Şaşkınlık İlkesi' için bunu insanlara söyleyeceğim.

+1 Bu benim de düşündüğüm şey, ayrıca yıkıcı olmak LINQ IEnumerable uzantılarının beklenen davranışını bozabilir. Normal / yıkıcı işlemleri yapmadan bu tür sıralı koleksiyon üzerinde tekrarlamak garip geliyor.
Steven Evers

1

Bir röportaj için bana verilen en ilginç kodlama zorluklarından biri, işlevsel bir kuyruk oluşturmaktı. Gereksinim, her enqueue çağrısının eski kuyruğu ve kuyruktaki yeni öğeyi içeren yeni bir kuyruk oluşturmasıydı. Dequeue ayrıca yeni bir kuyruk ve dequeued öğeyi bir çıkış parametresi olarak döndürür.

Bu uygulamadan bir IEnumerator oluşturmak tahribatsız olacaktır. Ve size iyi performans gösteren bir Fonksiyonel Kuyruk uygulamanızı söyleyeyim, performans gösteren bir İşlevsel Yığın uygulamaktan çok daha zor (yığın Push / Pop her iki Kuyruk üzerinde çalışır, kuyrukta bir Kuyruk Enqueue kuyrukta çalışır, kafasında çalışır).

Demek istediğim ... Kendi İşaretçi mekanizmanızı (StackNode <T>) uygulayarak ve Enumerator'da işlevsel anlambilimi kullanarak tahribatsız bir Stack Enumerator oluşturmak önemsizdir.

public class Stack<T> implements IEnumerator<T>
{
  private class StackNode<T>
  {
    private readonly T _data;
    private readonly StackNode<T> _next;
    public StackNode(T data, StackNode<T> next)
    {
       _data=data;
       _next=next;
    }
    public <T> Data{get {return _data;}}
    public StackNode<T> Next{get {return _Next;}}
  }

  private StackNode<T> _head;

  public void Push(T item)
  {
    _head =new StackNode<T>(item,_head);
  }

  public T Pop()
  {
    //Add in handling for a null head (i.e. fresh stack)
    var temp=_head.Data;
    _head=_head.Next;
    return temp;
  }

  ///Here's the fun part
  public IEnumerator<T> GetEnumerator()
  {
    //make a copy.
    var current=_head;
    while(current!=null)
    {
       yield return current.Data;
       current=_head.Next;
    }
  }    
}

Dikkat edilmesi gereken bazı şeyler. Current = _head ifadesinden önce itme veya açma çağrısı; tamamlamalar numaralandırma için size çoklu iş parçacığı olmadığından farklı bir yığın verir (buna karşı korumak için bir ReaderWriterLock kullanmak isteyebilirsiniz). StackNode içindeki alanları salt okunur yaptım ama elbette T değişken bir nesne ise değerlerini değiştirebilirsiniz. Parametre olarak bir StackNode alan bir Stack yapıcısı oluşturuyorsanız (ve düğümü başa iletilen başlığa ayarlarsanız). Bu şekilde inşa edilen iki yığın birbirini etkilemeyecektir (bahsettiğim gibi değişebilir bir T hariç). İstediğiniz her şeyi bir yığın halinde Push ve Pop yapabilirsiniz, diğeri değişmez.

Ve arkadaşım, bir Yığın tahribatsız numaralandırmasını nasıl yaptığınızı.


+1: Tahribatsız Stack ve Queue numaralandırıcılarım benzer şekilde uygulanır. Özel karşılaştırıcılar ile PQs / Heaps çizgileri boyunca daha fazla düşünüyordum.
Steven Evers

1
Aynı yaklaşımı kullan diyebilirim ... Daha önce bir Fonksiyonel PQ uyguladığımı söyleyemem ... Bu makalede , sıralı dizilerin doğrusal olmayan bir yaklaşım olarak kullanılmasını önerdiği
Michael Brown

0

İşleri "basit" tutmak amacıyla , en az dört tür nesne olmasına rağmen , .NET'in çağrılması GetEnumerator()ve ardından alınan nesne üzerinde MoveNextve Currentüzerinde kullanılması ile numaralandırılan şeyler için yalnızca bir genel / genel olmayan arabirim çifti vardır. yalnızca bu yöntemleri desteklemelidir:

  1. En az bir kez numaralandırılabilen, ancak mutlaka bundan daha fazla olmayan şeyler.
  2. Serbest iş parçacıklı bağlamlarda keyfi olarak sayılabilecek, ancak her seferinde keyfi olarak farklı içerik verebilecek şeyler
  3. Serbest iş parçacıklı bağlamlarda rasgele sayıda sayılabilen, ancak bunları art arda numaralandıran kod herhangi bir mutasyon yöntemi çağırmazsa, tüm numaralandırmaların aynı içeriği döndüreceğini garanti edebilir.
  4. İstenmeyen sayıda sayılabilen ve var oldukları sürece her seferinde aynı içeriği döndürmesi garanti edilen şeyler.

Daha yüksek numaralı tanımlardan birini karşılayan herhangi bir örnek, aynı zamanda daha düşük olanları da karşılayacaktır, ancak daha düşük tanımlardan birini karşılayan bir nesneyi gerektiren kod, daha düşük tanımlardan biri verildiğinde kesilebilir.

Microsoft, uygulayan sınıfların IEnumerable<T>ikinci tanımı karşılaması gerektiğine karar vermiş , ancak daha yüksek bir şeyi tatmin etmek zorunda değildir. Tartışmalı olarak, sadece ilk tanımı karşılayabilecek bir şeyin uygulamak IEnumerable<T>yerine fazla bir nedeni yoktur IEnumerator<T>; eğer foreachdöngüler kabul edebileceğini IEnumerator<T>, bu sadece basitçe ikinci arabirimini uygulamak için bir kez numaralandırılan şeyler için mantıklıdır. Ne yazık ki, yalnızca uygulayan türler IEnumerator<T>C # ve VB.NET'te kullanımı bir GetEnumeratoryöntemi olan türlere göre daha az uygundur .

Her halükarda, farklı garantiler verebilecek şeyler için farklı numaralandırılabilir türler ve / veya IEnumerable<T>hangi garantileri uygulayabileceğine dair bir örnek sormanın standart bir yolu olsa da yararlı olsa da, henüz böyle bir tür yoktur.


0

Bence bu, Liskov ikame ilkesinin ihlali anlamına gelir ,

bir programdaki nesneler, o programın doğruluğunu değiştirmeden alt türlerinin örnekleriyle değiştirilebilir olmalıdır.

Programımda aşağıdaki gibi birden fazla çağrılan bir yöntemim varsa,

PrintCollection<T>(IEnumerable<T> collection)
{
    foreach (T item in collection)
    {
        Console.WriteLine(item);
    }
}

Programın doğruluğunu değiştirmeden herhangi bir IEnumerable'ı değiştirebilmeliyim, ancak yıkıcı kuyruğu kullanmak program davranışını büyük ölçüde değiştirir.

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.