Share via


Bug kode deklaratif/imperatif campuran (LINQ ke XML)

LINQ ke XML berisi berbagai metode yang memungkinkan Anda memodifikasi pohon XML secara langsung. Anda dapat menambahkan elemen, menghapus elemen, mengubah konten elemen, menambahkan atribut, dan sebagainya. Antarmuka pemrograman ini dijelaskan dalam Memodifikasi pohon XML. Jika Anda melakukan perulangan melalui salah satu sumbu, seperti Elements, dan Anda memodifikasi pohon XML saat melakukan perulangan melalui sumbu, Anda bisa mendapatkan beberapa bug aneh.

Masalah ini terkadang dikenal sebagai "Masalah Halloween".

Saat Anda menulis beberapa kode menggunakan LINQ yang berulang melalui koleksi, Anda menulis kode dengan gaya deklaratif. Lebih mirip dengan menggambarkan apa yang Anda inginkan, alih-alh bagaimana Anda ingin menyelesaikannya. Jika Anda menulis kode yang 1) memahami elemen pertama, 2) mengujinya untuk beberapa kondisi, 3) memodifikasinya, dan 4) memasukkannya kembali ke dalam daftar, maka ini akan menjadi kode imperatif. Anda memberi tahu komputer bagaimana melakukan apa yang ingin Anda lakukan.

Mencampur gaya kode ini dalam operasi yang sama adalah hal yang menyebabkan masalah. Pertimbangkan hal berikut:

Misalnya Anda memiliki daftar tertaut dengan tiga item di dalamnya (a, b, dan c):

a -> b -> c

Sekarang, misalkan Anda ingin memindai daftar tertaut dan menambahkan tiga item baru (a ', b', dan c'). Anda ingin daftar tertaut yang dihasilkan terlihat seperti ini:

a -> a' -> b -> b' -> c -> c'

Jadi Anda menulis kode yang mengulang daftar, dan untuk setiap item, menambahkan item baru tepat setelahnya. Yang akan terjadi adalah bahwa pertama kode akan melihat elemen a dan menyisipkan a' setelahnya. Sekarang, kode Anda akan berpindah ke simpul berikutnya dalam daftar, yang sekarang berupa a', sehingga menambahkan item baru antara a' dan b ke daftar!

Bagaimana Anda akan menyelesaikan ini? Nah, Anda dapat membuat salinan daftar tertaut asli, dan membuat daftar yang sama sekali baru. Atau jika Anda menulis kode yang murni imperatif, Anda mungkin menemukan item pertama, menambahkan item baru, dan kemudian maju dua kali dalam daftar tertaut, kemudian melalui elemen yang baru saja Anda tambahkan.

Contoh: Menambahkan selagi mengulangi

Misalnya, Anda ingin menulis kode untuk membuat duplikat setiap elemen di pohon:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
foreach (XElement e in root.Elements())
    root.Add(new XElement(e.Name, (string)e));
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
For Each e As XElement In root.Elements()
    root.Add(New XElement(e.Name, e.Value))
Next

Kode ini masuk ke perulangan tak terbatas. Pernyataan foreach melakukan perulangan melalui sumbu Elements(), menambahkan elemen baru ke elemen doc. Ini akhirnya juga mengulangi elemen yang baru saja ditambahkan. Dan karena ini mengalokasikan objek baru dengan setiap perulangan dari perulangan, pada akhirnya akan mengonsumsi semua memori yang tersedia.

Anda dapat memperbaiki masalah ini dengan mengingat kembali koleksi menggunakan operator kueri standar ToList, sebagai berikut:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
foreach (XElement e in root.Elements().ToList())
    root.Add(new XElement(e.Name, (string)e));
Console.WriteLine(root);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
For Each e As XElement In root.Elements().ToList()
    root.Add(New XElement(e.Name, e.Value))
Next
Console.WriteLine(root)

Kini kodenya berfungsi. Pohon XML yang dihasilkan adalah sebagai berikut:

<Root>
  <A>1</A>
  <B>2</B>
  <C>3</C>
  <A>1</A>
  <B>2</B>
  <C>3</C>
</Root>

Contoh: Menghapus selagi mengulangi

Jika Anda ingin menghapus semua node pada tingkat tertentu, Anda mungkin tergoda untuk menulis kode seperti berikut:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
foreach (XElement e in root.Elements())
    e.Remove();
Console.WriteLine(root);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
For Each e As XElement In root.Elements()
    e.Remove()
Next
Console.WriteLine(root)

Namun, ini tidak melakukan apa yang Anda inginkan. Dalam situasi ini, setelah menghapus elemen pertama, A, elemen dihapus dari pohon XML yang terkandung dalam root, dan kode dalam metode Elemen yang melakukan perulangan tidak dapat menemukan elemen berikutnya.

Contoh ini menghasilkan output berikut:

<Root>
  <B>2</B>
  <C>3</C>
</Root>

Solusinya lagi adalah memanggil ToList untuk mewujudkan koleksi, sebagai berikut:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
foreach (XElement e in root.Elements().ToList())
    e.Remove();
Console.WriteLine(root);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
For Each e As XElement In root.Elements().ToList()
    e.Remove()
Next
Console.WriteLine(root)

Contoh ini menghasilkan output berikut:

<Root />

Atau, Anda dapat menghilangkan perulangan sama sekali dengan memanggil RemoveAll pada elemen induk:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
root.RemoveAll();
Console.WriteLine(root);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
root.RemoveAll()
Console.WriteLine(root)

Contoh: Mengapa LINQ tidak dapat secara otomatis menangani masalah ini

Salah satu pendekatannya adalah selalu mengingat semuanya alih-alih melakukan evaluasi dengan malas. Namun, ini akan sangat memakan biaya dalam hal performa dan penggunaan memori. Bahkan, jika LINQ, dan LINQ ke XML, mengambil pendekatan ini, ini akan gagal dalam situasi dunia nyata.

Pendekatan lain yang memungkin adalah memasukkan semacam sintaks transaksi ke dalam LINQ, dan meminta pengompilasi mencoba menganalisis kode untuk menentukan jika ada koleksi tertentu yang perlu terwujud. Namun, mencoba untuk menentukan semua kode yang memiliki efek samping bersifat sangat kompleks. Pertimbangkan gambar berikut:

var z =
    from e in root.Elements()
    where TestSomeCondition(e)
    select DoMyProjection(e);
Dim z = _
    From e In root.Elements() _
    Where (TestSomeCondition(e)) _
    Select DoMyProjection(e)

Kode analisis semacam itu perlu menganalisis metode TestSomeCondition dan DoMyProjection, dan semua metode yang dipanggil metode tersebut, untuk menentukan jika ada kode yang memiliki efek samping. Tetapi kode analisis tidak dapat hanya mencari kode apa pun yang memiliki efek samping. Kode perlu memilih kode yang memiliki efek samping pada elemen turunan dari root dalam situasi ini saja.

LINQ ke XML tidak mencoba melakukan analisis semacam itu. Anda memiliki hak untuk menghindari masalah ini.

Contoh: Menggunakan kode deklaratif untuk menghasilkan pohon XML baru alih-alih memodifikasi pohon yang ada

Untuk menghindari masalah seperti ini, jangan mencampur kode deklaratif dan imperatif, bahkan jika Anda tahu persis semantik koleksi Anda dan semantik metode yang memodifikasi pohon XML. Jika Anda menulis kode yang menghindari masalah, kode perlu dikelola oleh pengembang lain di masa depan, dan mereka mungkin tidak memahami masalahnya dengan jelas. Jika Anda mencampur gaya pengodean deklaratif dan imperatif, kode akan lebih rapuh. Jika Anda menulis kode yang mewujudkan koleksi sehingga masalah ini dihindari, beri komentar yang sesuai dalam kode, sehingga programmer pemeliharaan akan memahami masalah ini.

Jika performa dan pertimbangan lainnya memungkinkan, gunakan hanya kode deklaratif. Jangan ubah pohon XML yang sudah ada. Sebagai gantinya, buat yang baru seperti yang ditunjukkan dalam contoh berikut:

XElement root = new XElement("Root",
    new XElement("A", "1"),
    new XElement("B", "2"),
    new XElement("C", "3")
);
XElement newRoot = new XElement("Root",
    root.Elements(),
    root.Elements()
);
Console.WriteLine(newRoot);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
Dim newRoot As XElement = New XElement("Root", _
    root.Elements(), root.Elements())
Console.WriteLine(newRoot)