Serializācija un saraksta elementu dublēšanās
Sveiki! Sen neesmu rakstījis un varētu atrast vairākus objektīvus un subjektīvus iemeslus sevis attaisnošanai. Tomēr interesantāk par iemesliem ir pastāstīt par .NET platformas gotču, ar kuru nupat sastapos.
Iespējams, arī Tev ir gadījies sastapties ar situāciju, kad "pa drāti" ir jānodod objekts ar datiem. Šādos gadījumos izmanto XML serializāciju, kas ļauj objektu pārvērst par XML dokumentu un otrādi. Mēs ar kolēģiem tā arī darījām, līdz šodien uzdūrāmies problēmai, kad pie deserializācijas tiek dublētas vērtības List<T> tipa īpašībai. Pieļauju, ka programmētājiem problēmu uzskatāmāk demonstrēs kods.
using System.Collections.Generic;
namespace TestSerialization
{
public class ListContainer
{
private List<int> _lazyList;
public List<int> LazyList
{
get
{
if (_lazyList == null)
{
_lazyList = new List<int>();
_lazyList.Add(1);
_lazyList.Add(6);
}
return _lazyList;
}
set { _lazyList = value; }
}
}
}
Tātad, vienkārša klase, kas sastāv no vienas īpašības, kas tiek aizpildīta pirmajā pieprasījumā. Domāju, ka līdzīgu kodu visi ir rakstījuši. Lai pārbaudītu serializāciju, uzrakstīju vienumtestu:
[TestMethod]
public void LazyListTest()
{
ListContainer target = new ListContainer();
int expected = target.LazyList.Count;
XmlSerializer serializer = new XmlSerializer(target.GetType());
string xmlString;
using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
{
serializer.Serialize(writer, target);
xmlString = writer.ToString();
}
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlString);
ListContainer result = (ListContainer)serializer.Deserialize(new XmlNodeReader(doc));
int actual = result.LazyList.Count;
Assert.AreEqual(expected, actual,"Item count differs");
}
Palaižot testu tas izgāžas, jo deserializētajā objektā ir divreiz vairāk elementu, nekā oriģinālajā objektā. Kāpēc tā?
Loģiski liekas, ka saraksta deserializācijai vajadzētu notikt apmēram šādi:
- Izveidojam objektu ListContainer
- Izveidojam jaunu sarakstu ar LazyList elementu vērtībām
- Piešķiram jaunizveidoto sarakstu ListContainer.LazyList īpašībai
Tādā gadījumā augstāk parādītais tests darbotos. Tomēr sūrā dzīves realitāte rāda, ka tas tā nav, jo patiesībā deserializācija notiek pēc šāda scenārija:
- Izveidojam ListContainer objektu
- Pievienojam XMLā esošos elementus sarakstam, kuram piekļūstam izmantojot ListContainer.LazyList get aksesoru (nezinu, kā latviski pateikt)
Līdz ar to kļūst skaidrs, kāpēc elementi dublējas - sākumā tiek sapildītas noklusētās vērtības un pēc tam pievienotas XML dokumentā esošās.
Diagnoze ir noteikta, jāsāk ārstēt. Laikam vienkāršākais risinājums ir noslēpt sarakstu no serializātora izmantojot atribūtu XmlIgnore, un serializācijai izveidot jaunu īpašību - masīvu. Šo masīvu izmantos tikai lai apmanītu serializāciju un tam nebūs atsevišķa lauka, kurā glabāt vērtības. Tātad, uzlabotais kods:
using System.Collections.Generic;
using System.Xml.Serialization;
namespace TestSerialization
{
public class ListContainer
{
private List<int> _lazyList;
[XmlIgnore]
public List<int> LazyList
{
get
{
if (_lazyList == null)
{
_lazyList = new List<int>();
_lazyList.Add(1);
_lazyList.Add(6);
}
return _lazyList;
}
set { _lazyList = value; }
}
[XmlArray (ElementName = "LazyList")]
public int[] LazyListDisguisedAsArray
{
get
{
int[] items = new int[LazyList.Count];
LazyList.CopyTo(items, 0);
return items;
}
set
{
_lazyList = new List<int>(value);
}
}
}
}
Kā redzams, ir realizēts viss iepriekš aprakstītais un izveidots papildus slānis, lai serializācija domātu, ka saraksts glabājas masīvā. Lai ģenerētais XML dokuments izskatītos smukāk un tajā nebūtu redzamas neglītās implementācijas detaļas, LazyListDisguisedAsArray tiek maskēts, izmantojot XmlArray atribūtu un norādītu elementa nosaukumu. Atkārtoti palaižot testu redzam, ka pacients ir izārstēts un var doties mājās.