Apakah iterator memiliki kontrak tersirat yang tidak merusak?
11
Katakanlah saya sedang merancang struktur data khusus seperti tumpukan atau antrian (misalnya - dapat berupa kumpulan pesanan sewenang-wenang lainnya yang memiliki padanan logis pushdan popmetode - yaitu metode accessor destruktif).
Jika Anda menerapkan iterator (dalam. NET, khususnya IEnumerable<T>) atas koleksi ini yang muncul pada setiap iterasi, apakah itu melanggar IEnumerable<T>kontrak yang tersirat?
Apakah IEnumerable<T>ada kontrak yang tersirat ini?
misalnya:
public IEnumerator<T> GetEnumerator()
{
if (this.list.Count > 0)
yield return this.Pop();
else
yield break;
}
Saya percaya seorang Enumerator yang destruktif melanggar Prinsip Terkecil Leasing . Sebagai contoh yang dibuat-buat, bayangkan perpustakaan objek bisnis yang menawarkan fungsi kenyamanan umum. Saya dengan polos menulis fungsi yang memperbarui koleksi objek bisnis:
static void UpdateStatus<T>(IEnumerable<T> collection) where T : IBusinessObject
{
foreach (var item in collection)
{
item.Status = BusinessObjectStatus.Foo;
}
}
Pengembang lain yang tidak tahu apa-apa tentang implementasi saya atau implementasi Anda dengan polos memutuskan untuk menggunakan keduanya. Kemungkinan mereka akan terkejut dengan hasilnya:
//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);
Bahkan jika pengembang menyadari penghitungan yang merusak, mereka mungkin tidak memikirkan dampaknya ketika menyerahkan koleksi ke fungsi kotak hitam. Sebagai penulis UpdateStatus, saya mungkin tidak akan mempertimbangkan penghitungan destruktif dalam desain saya.
Namun, itu hanya kontrak yang tersirat. Koleksi .NET, termasuk Stack<T>, menegakkan kontrak eksplisit dengan InvalidOperationException- "Koleksi mereka diubah setelah enumerator dipakai". Anda dapat berargumen bahwa seorang profesional sejati memiliki sikap emptor peringatan terhadap kode apa pun yang bukan miliknya. Kejutan dari pencacah yang merusak akan ditemukan dengan pengujian minimal.
+1 - untuk 'Prinsip Terkecil Ketertinggalan' saya akan mengutipnya pada orang-orang.
+1 Ini pada dasarnya adalah apa yang saya pikirkan juga, menjadi destruktif dapat mematahkan perilaku yang diharapkan dari ekstensi LINQ IEnumerable. Rasanya aneh untuk beralih pada jenis koleksi yang dipesan ini tanpa melakukan operasi normal / destruktif.
Steven Evers
1
Salah satu tantangan pengkodean yang paling menarik yang diberikan kepada saya untuk wawancara adalah membuat antrian fungsional. Syaratnya adalah bahwa setiap panggilan ke enqueue akan membuat antrian baru yang sesuai dengan antrian lama dan item baru di bagian ekor. Dequeue juga akan mengembalikan antrian baru dan item yang keluar sebagai param.
Membuat IEnumerator dari implementasi ini tidak merusak. Dan izinkan saya memberi tahu Anda menerapkan Antrian Fungsional yang berkinerja baik jauh lebih sulit daripada menerapkan Fungsional Stack yang performan (tumpukan Push / Pop keduanya bekerja pada Tail, untuk Antrian Enqueue bekerja pada ekor, dequeue bekerja di kepala).
Maksud saya ... ini sepele untuk membuat Stack Enumerator yang tidak rusak dengan mengimplementasikan mekanisme Pointer Anda sendiri (StackNode <T>) dan menggunakan semantik fungsional di Enumerator.
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;
}
}
}
Beberapa hal yang perlu diperhatikan. Panggilan untuk mendorong atau pop sebelum pernyataan saat ini = _head; selesai akan memberi Anda tumpukan yang berbeda untuk enumerasi daripada jika tidak ada multithreading (Anda mungkin ingin menggunakan ReaderWriterLock untuk melindungi ini). Saya membuat bidang di StackNode hanya untuk dibaca tetapi tentu saja jika T adalah objek yang bisa berubah, Anda dapat mengubah nilainya. Jika Anda membuat konstruktor Stack yang menggunakan StackNode sebagai parameter (dan atur head ke yang dilewatkan dalam node). Dua tumpukan yang dibangun dengan cara ini tidak akan berdampak satu sama lain (dengan pengecualian T yang bisa berubah seperti yang saya sebutkan). Anda dapat Push dan Pop semua yang Anda inginkan dalam satu tumpukan, yang lain tidak akan berubah.
Dan bahwa teman saya adalah bagaimana Anda melakukan penghitungan Stack yang tidak merusak.
+1: Pencacah Antrian dan Antrian saya yang tidak merusak diimplementasikan dengan cara yang sama. Saya lebih memikirkan jalur PQ / Tumpukan dengan pembanding khusus.
Steven Evers
1
Saya akan mengatakan menggunakan pendekatan yang sama ... tidak bisa mengatakan saya telah mengimplementasikan PQ Fungsional sebelumnya ... sepertinya artikel ini menyarankan menggunakan urutan yang dipesan sebagai pendekatan non-linear
Michael Brown
0
Dalam upaya untuk menjaga hal-hal "sederhana", .NET hanya memiliki satu pasang antarmuka generik / non-generik untuk hal-hal yang disebutkan dengan memanggil GetEnumerator()dan kemudian menggunakan MoveNextdan Currentpada objek yang diterima darinya, meskipun setidaknya ada setidaknya empat jenis objek yang seharusnya hanya mendukung metode-metode tersebut:
Hal-hal yang dapat disebutkan setidaknya satu kali, tetapi tidak harus lebih dari itu.
Hal-hal yang dapat disebutkan berulang kali secara sewenang-wenang dalam konteks utas, tetapi dapat secara sewenang-wenang menghasilkan konten yang berbeda setiap kali
Hal-hal yang dapat disebutkan berulang kali secara acak dalam konteks bebas-ulir, tetapi dapat menjamin bahwa jika kode yang menyebutkannya berulang kali tidak menyebut metode mutasi, semua enumerasi akan mengembalikan konten yang sama.
Hal-hal yang dapat disebutkan beberapa kali secara sewenang-wenang, dan dijamin untuk mengembalikan konten yang sama setiap kali selama ada.
Contoh apa pun yang memenuhi salah satu definisi yang lebih tinggi juga akan memenuhi semua yang lebih rendah, tetapi kode yang membutuhkan objek yang memenuhi salah satu definisi yang lebih tinggi dapat rusak jika diberikan salah satu yang lebih rendah.
Tampaknya Microsoft telah memutuskan bahwa kelas yang mengimplementasikan IEnumerable<T>harus memenuhi definisi kedua, tetapi tidak berkewajiban untuk memenuhi sesuatu yang lebih tinggi. Dapat diperdebatkan, tidak ada banyak alasan bahwa sesuatu yang hanya bisa memenuhi definisi pertama harus diimplementasikan IEnumerable<T>daripada IEnumerator<T>; jika foreachloop dapat menerima IEnumerator<T>, akan masuk akal untuk hal-hal yang hanya dapat dihitung satu kali untuk hanya mengimplementasikan antarmuka yang terakhir. Sayangnya, tipe yang hanya mengimplementasikan IEnumerator<T>kurang nyaman digunakan dalam C # dan VB.NET dibandingkan tipe yang memiliki GetEnumeratormetode.
Dalam kasus apa pun, meskipun akan berguna jika ada jenis enumerable berbeda untuk hal-hal yang dapat membuat jaminan berbeda, dan / atau cara standar untuk menanyakan sebuah contoh yang mengimplementasikan IEnumerable<T>jaminan apa yang dapat dibuatnya, tidak ada jenis seperti yang ada.
Saya harus bisa menukar semua IEnumerable tanpa mengubah kebenaran program, tetapi menggunakan antrian destruktif akan mengubah perilaku program secara luas.
Salah satu tantangan pengkodean yang paling menarik yang diberikan kepada saya untuk wawancara adalah membuat antrian fungsional. Syaratnya adalah bahwa setiap panggilan ke enqueue akan membuat antrian baru yang sesuai dengan antrian lama dan item baru di bagian ekor. Dequeue juga akan mengembalikan antrian baru dan item yang keluar sebagai param.
Membuat IEnumerator dari implementasi ini tidak merusak. Dan izinkan saya memberi tahu Anda menerapkan Antrian Fungsional yang berkinerja baik jauh lebih sulit daripada menerapkan Fungsional Stack yang performan (tumpukan Push / Pop keduanya bekerja pada Tail, untuk Antrian Enqueue bekerja pada ekor, dequeue bekerja di kepala).
Maksud saya ... ini sepele untuk membuat Stack Enumerator yang tidak rusak dengan mengimplementasikan mekanisme Pointer Anda sendiri (StackNode <T>) dan menggunakan semantik fungsional di Enumerator.
Beberapa hal yang perlu diperhatikan. Panggilan untuk mendorong atau pop sebelum pernyataan saat ini = _head; selesai akan memberi Anda tumpukan yang berbeda untuk enumerasi daripada jika tidak ada multithreading (Anda mungkin ingin menggunakan ReaderWriterLock untuk melindungi ini). Saya membuat bidang di StackNode hanya untuk dibaca tetapi tentu saja jika T adalah objek yang bisa berubah, Anda dapat mengubah nilainya. Jika Anda membuat konstruktor Stack yang menggunakan StackNode sebagai parameter (dan atur head ke yang dilewatkan dalam node). Dua tumpukan yang dibangun dengan cara ini tidak akan berdampak satu sama lain (dengan pengecualian T yang bisa berubah seperti yang saya sebutkan). Anda dapat Push dan Pop semua yang Anda inginkan dalam satu tumpukan, yang lain tidak akan berubah.
Dan bahwa teman saya adalah bagaimana Anda melakukan penghitungan Stack yang tidak merusak.
sumber
Dalam upaya untuk menjaga hal-hal "sederhana", .NET hanya memiliki satu pasang antarmuka generik / non-generik untuk hal-hal yang disebutkan dengan memanggil
GetEnumerator()
dan kemudian menggunakanMoveNext
danCurrent
pada objek yang diterima darinya, meskipun setidaknya ada setidaknya empat jenis objek yang seharusnya hanya mendukung metode-metode tersebut:Contoh apa pun yang memenuhi salah satu definisi yang lebih tinggi juga akan memenuhi semua yang lebih rendah, tetapi kode yang membutuhkan objek yang memenuhi salah satu definisi yang lebih tinggi dapat rusak jika diberikan salah satu yang lebih rendah.
Tampaknya Microsoft telah memutuskan bahwa kelas yang mengimplementasikan
IEnumerable<T>
harus memenuhi definisi kedua, tetapi tidak berkewajiban untuk memenuhi sesuatu yang lebih tinggi. Dapat diperdebatkan, tidak ada banyak alasan bahwa sesuatu yang hanya bisa memenuhi definisi pertama harus diimplementasikanIEnumerable<T>
daripadaIEnumerator<T>
; jikaforeach
loop dapat menerimaIEnumerator<T>
, akan masuk akal untuk hal-hal yang hanya dapat dihitung satu kali untuk hanya mengimplementasikan antarmuka yang terakhir. Sayangnya, tipe yang hanya mengimplementasikanIEnumerator<T>
kurang nyaman digunakan dalam C # dan VB.NET dibandingkan tipe yang memilikiGetEnumerator
metode.Dalam kasus apa pun, meskipun akan berguna jika ada jenis enumerable berbeda untuk hal-hal yang dapat membuat jaminan berbeda, dan / atau cara standar untuk menanyakan sebuah contoh yang mengimplementasikan
IEnumerable<T>
jaminan apa yang dapat dibuatnya, tidak ada jenis seperti yang ada.sumber
Saya pikir ini akan menjadi pelanggaran prinsip substitusi Liskov yang menyatakan,
Jika saya memiliki metode seperti berikut yang disebut lebih dari sekali dalam program saya,
Saya harus bisa menukar semua IEnumerable tanpa mengubah kebenaran program, tetapi menggunakan antrian destruktif akan mengubah perilaku program secara luas.
sumber