Aracılığıyla paylaş


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. Eğer Elements gibi eksenlerden birinde yineleniyorsanız ve eksende yinelenirken XML ağacını değiştiriyorsanız, bazı garip hatalarla karşılaşabilirsiniz.

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ğıdakileri göz önünde bulundurun:

üç öğ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. Olan şudur ki, kodunuz önce a öğesini görecek ve ardından a' öğesini ekleyecektir. Ş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.

Bir ö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, Elements() ekseninde yinelenir ve yeni öğeler doc öğesine eklenir. Yeni eklenen öğeler üzerinde de yinelemeye devam eder. 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 işlemi

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ğıda belirtildiği gibi somutlaştırmak için ToList çağrısı yapmaktır.

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, RemoveAll işlevini üst öğede çağırarak 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 sadece yan etkileri olan herhangi bir kodu arayamazdı. Bu durumda, yalnızca root alt öğeleri üzerinde yan etkileri olan kodu seçmek 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. Eğer problemlerden kaçınan bir kod yazarsanız, kodunuzun gelecekte diğer geliştiriciler tarafından bakıma alınması gerekecek ancak onlar sorunlara sizin kadar hakim 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ın sorunu anlaması için kodunuzda uygun açıklamaları ekleyin.

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)