Karışık bildirim temelli/kesinlik temelli kod hataları (LINQ to XML)

LINQ to XML, bir XML ağacını doğrudan değiştirmenize olanak sağlayan çeşitli yöntemler içerir. Öğe ekleyebilir, öğeleri silebilir, bir öğenin içeriğini değiştirebilir, öznitelikler ekleyebilir vb. Bu programlama arabirimi XML ağaçlarını değiştirme bölümünde açıklanmıştır. gibi Elementseksenlerden birinde yineleniyorsanız ve eksende yinelenirken XML ağacını değiştiriyorsanız, bazı garip hatalarla sonuçlanabilirsiniz.

Bu sorun bazen "Cadılar Bayramı Sorunu" olarak bilinir.

LINQ kullanarak bir koleksiyonda yineleyen bir kod yazdığınızda, kodu bildirim temelli bir stilde yazarsınız. Ne istediğinizi açıklamaya daha çok, bunu nasıl yapmak istediğinizi açıklamaya çok daha yakın. 1) ilk öğeyi alır, 2) bir koşul için sınar, 3) değiştirir ve 4) listeye geri koyan kod yazarsanız, bu kesinlik temelli bir kod olacaktır. Bilgisayara istediğini nasıl yapacağını söylüyorsun.

Bu kod stillerini aynı işlemde karıştırmak sorunlara yol açar. Aşağıdaki topluluklara bir göz atın:

üç öğe içeren bağlı bir listenize (a, b ve c) sahip olduğunuzu varsayalım:

a -> b -> c

Şimdi, üç yeni öğe (a', b've c') ekleyerek bağlantılı listede gezinmek istediğinizi varsayalım. Sonuçta elde edilen bağlantılı listenin şu şekilde görünmesini istiyorsunuz:

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

Böylece listede yineleyen bir kod yazarsınız ve her öğe için hemen arkasına yeni bir öğe eklersiniz. Ne olur, kodunuzun önce öğesini göreceği a ve sonra ekleyecek a' olmasıdır. Şimdi kodunuz listede bir sonraki düğüme taşınır ve bu da şu anda a'olur, böylece listeye a' ile b arasına yeni bir öğe ekler!

Bunu nasıl çözersin? Özgün bağlantılı listenin bir kopyasını oluşturabilir ve tamamen yeni bir liste oluşturabilirsiniz. Ya da tamamen kesinlik temelli kod yazıyorsanız, ilk öğeyi bulabilir, yeni öğeyi ekleyebilir ve ardından yeni eklediğiniz öğe üzerinde ilerleyerek bağlantılı listede iki kez ilerleyebilirsiniz.

Örnek: Yinelenirken ekleme

Örneğin, bir ağaçtaki her öğenin kopyasını oluşturmak için kod yazmak istediğinizi varsayalım:

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

Bu kod sonsuz bir döngüye girer. foreach deyimi, öğeye Elements() yeni öğeler doc ekleyerek eksende yinelenir. Yeni eklediği öğeler aracılığıyla da yinelenir. Döngünün her yinelemesiyle yeni nesneler ayırdığından, sonunda tüm kullanılabilir belleği tüketir.

Aşağıdaki gibi standart sorgu işlecini kullanarak ToList koleksiyonu belleğe çekerek bu sorunu çözebilirsiniz:

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)

Artık kod çalışıyor. Sonuçta elde edilen XML ağacı aşağıdaki gibidir:

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

Örnek: Yinelenirken silme

Belirli bir düzeydeki tüm düğümleri silmek istiyorsanız, aşağıdaki gibi bir kod yazmak isteyebilirsiniz:

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)

Ancak, bu istediğiniz şeyi yapmaz. Bu durumda, ilk öğe olan A'yı kaldırdıktan sonra, kökte bulunan XML ağacından kaldırılır ve yinelemeyi yerine getiren Elements yöntemindeki kod bir sonraki öğeyi bulamaz.

Bu örnek aşağıdaki çıkışı oluşturur:

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

Çözüm, koleksiyonu aşağıdaki gibi gerçekleştirmeye çağırmaktır ToList :

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)

Bu örnek aşağıdaki çıkışı oluşturur:

<Root />

Alternatif olarak, üst öğeyi çağırarak RemoveAll yinelemeyi tamamen ortadan kaldırabilirsiniz:

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)

Örnek: LINQ neden bu sorunları otomatik olarak işleyemiyor?

Bir yaklaşım, yavaş değerlendirme yapmak yerine her şeyi her zaman belleğe getirmek olacaktır. Ancak, performans ve bellek kullanımı açısından çok pahalı olacaktır. Aslında LINQ ve LINQ to XML bu yaklaşımı benimseseydi gerçek dünyada başarısız olurdu.

Başka bir olası yaklaşım LINQ içine bir tür işlem söz dizimi koymak ve derleyicinin belirli bir koleksiyonun gerçekleştirilmesi gerekip gerekmediğini belirlemek için kodu analiz etmeye çalışmaktır. Ancak yan etkileri olan tüm kodları belirlemeye çalışmak son derece karmaşıktır. Aşağıdaki kodu inceleyin:

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)

Bu tür analiz kodunun, herhangi bir kodun yan etkileri olup olmadığını belirlemek için TestSomeCondition ve DoMyProjection yöntemlerini ve bu yöntemlerin çağrıldığı tüm yöntemleri çözümlemesi gerekir. Ancak analiz kodu yalnızca yan etkileri olan herhangi bir kodu arayamadı. Yalnızca bu durumda alt öğeleri üzerinde yan etkileri olan kodu seçmesi root gerekir.

LINQ to XML böyle bir çözümleme gerçekleştirmeye çalışmaz. Bu sorunlardan kaçınmak size bağlı.

Örnek: Var olan ağacı değiştirmek yerine yeni bir XML ağacı oluşturmak için bildirim temelli kod kullanın

Bu tür sorunları önlemek için, koleksiyonlarınızın semantiğini ve XML ağacını değiştiren yöntemlerin semantiğini tam olarak biliyor olsanız bile bildirim temelli ve kesinlik temelli kodu karıştırmayın. Sorunlardan kaçınan bir kod yazarsanız, kodunuzun gelecekte diğer geliştiriciler tarafından korunması gerekir ve bunlar sorunlarda o kadar net olmayabilir. Bildirim temelli ve kesinlik temelli kodlama stillerini karıştırırsanız kodunuz daha kırılgan olur. Bu sorunlardan kaçınılması için bir koleksiyonu gerçekleştiren kod yazarsanız, bakım programcıların sorunu anlaması için kodunuzda uygun açıklamaları not edin.

Performans ve diğer önemli noktalar izin verirse, yalnızca bildirim temelli kodu kullanın. Var olan XML ağacınızı değiştirmeyin. Bunun yerine, aşağıdaki örnekte gösterildiği gibi yeni bir tane oluşturun:

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)