[ Team LiB ] |
21.6 SerializationWhen an object is streamed to disk, its various member data must be serialized—that is, written out to the stream as a series of bytes. The object will also be serialized when stored in a database or when marshaled across a context, app domain, process, or machine boundary. The CLR provides support for serializing an object-graph—an object and all the member data of that object. As noted in Chapter 19, by default, types are not serialized. To serialize an object, you must explicitly mark it with the [Serializable] attribute. In either case, the CLR will do the work of serializing your object for you. Because the CLR knows how to serialize all the primitive types, if your object consists of nothing but primitive types (all your member data consists of integers, longs, strings, etc.), you're all set. If your object consists of other user-defined types (classes), you must ensure that these types are also serializable. The CLR will try to serialize each object contained by your object (and all their contained objects as well), but these objects themselves must either be primitive types or they must be serializable. This was also evident in Chapter 19 when you marshaled a Shape object that contained a Point object as member data. The Point object in turn consisted of primitive data. In order to serialize (and thus marshal) the Shape object, its constituent member, the Point object, also had to be marked as serializable.
21.6.1 Using a FormatterWhen data is serialized, it will eventually be read, either by the same program or by a different program running on another machine. In any case, the code reading the data will expect that data to be in a particular format. Most of the time in a .NET application, the expected format will either be native binary format or Simple Object Access Protocol (SOAP).
When data is serialized, the format of the serialization is determined by the formatter you apply. In Chapter 19, you used formatters with channels when communicating with a remote object. Formatter classes implement the interface IFormatter; you are also free to create your own formatter, though very few programmers will ever need or want to! The CLR provides both a SoapFormatter for Internet serialization and a BinaryFormatter that is useful for fast local storage. You can instantiate these objects with their default constructors: BinaryFormatter binaryFormatter = New BinaryFormatter( ); Once you have an instance of a formatter, you can invoke its Serialize( ) method, passing in a stream and an object to serialize. You'll see how this is done in the next example. 21.6.2 Working with SerializationTo see serialization at work, you need a sample class that you can serialize and then deserialize. You can start by creating a class named SumOf. SumOf has three member variables: private int startNumber = 1; private int endNumber; private int[] theSums; The member array theSums represents the value of the sums of all the numbers from startNumber through endNumber. Thus, if startNumber is 1 and endNumber is 10, the array will have the values: 1,3,6,10,15,21,28,36,45,55 Each value is the sum of the previous value plus the next in the series. Thus if the series is 1,2,3,4, the first value in theSums will be 1. The second value is the previous value (1) plus the next in the series (2); thus, theSums[1] will hold the value 3. Likewise, the third value is the previous value (3) plus the next in the series, so theSums[2] is 6. Finally, the fourth value in theSums is the previous value (6) plus the next in the series (4), for a value of 10. The constructor for the SumOf object takes two integers: the starting number and the ending number. It assigns these to the local values and then calls a helper function to compute the contents of the array: public SumOf(int start, int end) { startNumber = start; endNumber = end; ComputeSums( ); The ComputeSums helper function fills in the contents of the array by computing the sums in the series from startNumber through endNumber: private void ComputeSums( ) { int count = endNumber - startNumber + 1; theSums = new int[count]; theSums[0] = startNumber; for (int i=1,j=startNumber + 1;i<count;i++,j++) { theSums[i] = j + theSums[i-1]; } } You can display the contents of the array at any time by using a foreach loop: private void DisplaySums( ) { foreach(int i in theSums) { Console.WriteLine("{0}, ",i); } } 21.6.2.1 Serializing the objectNow, mark the class as eligible for serialization with the [Serializable] attribute: [Serializable] class SumOf To invoke serialization, you first need a file stream object into which you'll serialize the SumOf object: FileStream fileStream = new FileStream("DoSum.out",FileMode.Create); You are now ready to call the formatter's Serialize( ) method, passing in the stream and the object to serialize. Because this will be done in a method of SumOf, you can pass in the this object, which points to the current object: binaryFormatter.Serialize(fileStream,this); This will serialize the SumOf object to disk. 21.6.2.2 Deserializing the objectTo reconstitute the object, open the file and ask a binary formatter to DeSerialize it: public static SumOf DeSerialize( ) { FileStream fileStream = new FileStream("DoSum.out",FileMode.Open); BinaryFormatter binaryFormatter = new BinaryFormatter( ); SumOf retVal = (SumOf) binaryFormatter.Deserialize(fileStream); fileStream.Close( ); return retVal; } To make sure all this works, first instantiate a new object of type SumOf and tell it to serialize itself. Then create a new instance of type SumOf by calling the static deserializer and asking it to display its values: public static void Main( ) { Console.WriteLine("Creating first one with new..."); SumOf app = new SumOf(1,10); Console.WriteLine( "Creating second one with deserialize..."); SumOf newInstance = SumOf.DeSerialize( ); newInstance.DisplaySums( ); } Example 21-15 provides the complete source code to illustrate serialization and deserialization. Example 21-15. Serializing and deserializing an objectnamespace Programming_CSharp { using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; [Serializable] class SumOf { private int startNumber = 1; private int endNumber; private int[] theSums; public static void Main( ) { Console.WriteLine("Creating first one with new..."); SumOf app = new SumOf(1,10); Console.WriteLine("Creating second one with deserialize..."); SumOf newInstance = SumOf.DeSerialize( ); newInstance.DisplaySums( ); } public SumOf(int start, int end) { startNumber = start; endNumber = end; ComputeSums( ); DisplaySums( ); Serialize( ); } private void ComputeSums( ) { int count = endNumber - startNumber + 1; theSums = new int[count]; theSums[0] = startNumber; for (int i=1,j=startNumber + 1;i<count;i++,j++) { theSums[i] = j + theSums[i-1]; } } private void DisplaySums( ) { foreach(int i in theSums) { Console.WriteLine("{0}, ",i); } } private void Serialize( ) { Console.Write("Serializing..."); // create a file stream to write the file FileStream fileStream = new FileStream("DoSum.out",FileMode.Create); // use the CLR binary formatter BinaryFormatter binaryFormatter = new BinaryFormatter( ); // serialize to disk binaryFormatter.Serialize(fileStream,this); Console.WriteLine("...completed"); fileStream.Close( ); } public static SumOf DeSerialize( ) { FileStream fileStream = new FileStream("DoSum.out",FileMode.Open); BinaryFormatter binaryFormatter = new BinaryFormatter( ); SumOf retVal = (SumOf) binaryFormatter.Deserialize(fileStream); fileStream.Close( ); return retVal; } } } Output: Creating first one with new... 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, Serializing......completed Creating second one with deserialize... 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, The output shows that the object was created, displayed, and then serialized. The object was then deserialized and output again, with no loss of data. 21.6.3 Handling Transient DataIn some ways, the approach to serialization demonstrated in Example 21-15 is very wasteful. Because you can compute the contents of the array given its starting and ending numbers, there really is no reason to store its elements to disk. Although the operation might be inexpensive with a small array, it could become costly with a very large one. You can tell the serializer not to serialize some data by marking it with the [NonSerialized] attribute: [NonSerialized] private int[] theSums; If you don't serialize the array, however, the object you create will not be correct when you deserialize it. The array will be empty. Remember, when you deserialize the object, you simply read it up from its serialized form; no methods are run. To fix the object before you return it to the caller, implement the IDeserializationCallback interface: [Serializable] class SumOf : IDeserializationCallback Also implement the one method of this interface: OnDeserialization( ). The CLR promises that if you implement this interface, your class's OnDeserialization( ) method will be called when the entire object graph has been deserialized. This is just what you want: the CLR will reconstitute what you've serialized, and then you have the opportunity to fix up the parts that were not serialized. This implementation can be very simple. Just ask the object to recompute the series: public virtual void OnDeserialization (Object sender) { ComputeSums( ); } This is a classic space/time trade-off; by not serializing the array, you make deserialization somewhat slower (because you must take the time to recompute the array), and you make the file somewhat smaller. To see if not serializing the array had any effect, I ran the program with the digits 1 to 5,000. Before setting [NonSerialized] on the array, the serialized file was 20K. After setting [NonSerialized], the file was 1K. Not bad. Example 21-16 shows the source code using the digits 1 to 5 as input (to simplify the output). Example 21-16. Working with a nonserialized objectnamespace Programming_CSharp { using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; [Serializable] class SumOf : IDeserializationCallback { private int startNumber = 1; private int endNumber [NonSerialized] private int[] theSums; public static void Main( ) { Console.WriteLine("Creating first one with new..."); SumOf app = new SumOf(1,5); Console.WriteLine("Creating second one with deserialize..."); SumOf newInstance = SumOf.DeSerialize( ); newInstance.DisplaySums( ); } public SumOf(int start, int end) { startNumber = start; endNumber = end; ComputeSums( ); DisplaySums( ); Serialize( ); } private void ComputeSums( ) { int count = endNumber - startNumber + 1; theSums = new int[count]; theSums[0] = startNumber; for (int i=1,j=startNumber + 1;i<count;i++,j++) { theSums[i] = j + theSums[i-1]; } } private void DisplaySums( ) { foreach(int i in theSums) { Console.WriteLine("{0}, ",i); } } private void Serialize( ) { Console.Write("Serializing..."); // create a file stream to write the file FileStream fileStream = new FileStream("DoSum.out",FileMode.Create); // use the CLR binary formatter BinaryFormatter binaryFormatter = new BinaryFormatter( ); // serialize to disk binaryFormatter.Serialize(fileStream,this); Console.WriteLine("...completed"); fileStream.Close( ); } public static SumOf DeSerialize( ) { FileStream fileStream = new FileStream("DoSum.out",FileMode.Open); BinaryFormatter binaryFormatter = new BinaryFormatter( ); SumOf retVal = (SumOf) binaryFormatter.Deserialize(fileStream); fileStream.Close( ); return retVal; } // fix up the nonserialized data public virtual void OnDeserialization (Object sender) { ComputeSums( ); } } } Output: Creating first one with new... 1, 3, 6, 10, 15, Serializing......completed Creating second one with deserialize... 1, 3, 6, 10, 15, You can see in the output that the data was successfully serialized to disk and then reconstituted by deserialization. The trade-off of disk storage space versus time does not make a lot of sense with five values, but it makes a great deal of sense with five million values. So far you've streamed your data to disk for storage and across the network for easy communication with distant programs. There is one other time you might create a stream: to store permanent configuration and status data on a per-user basis. For this purpose, the .NET Frameworks offer isolated storage. |
[ Team LiB ] |