Tải bản đầy đủ - 0 (trang)
13-1. Implement a Custom Serializable Type

13-1. Implement a Custom Serializable Type

Tải bản đầy đủ - 0trang

CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS







Fields that contain unserializable data types







Fields that contain values that might be invalid when the object is deserialized,

such as database connections, memory addresses, thread IDs, and unmanaged

resource handles







Fields that contain sensitive or secret information, such as passwords, encryption

keys, and the personal details of people and organizations







Fields that contain data that is easily re-creatable or retrievable from other

sources, especially if there is a lot of data



■ Note See Recipe 2-14 for an example of using a slightly different method to exclude members from serialization

when using the JSON format.



If you exclude fields from serialization, you must implement your type to compensate for the fact

that some data will not be present when an object is deserialized. Unfortunately, you cannot create or

retrieve the missing data fields in an instance constructor, because formatters do not call constructors

during the process of deserializing objects. The best approach for achieving fine-grained control of the

serialization of your custom types is to use the attributes from the System.Runtime.Serialization

namespace described in Table 13-1. These attributes allow you to identify methods of the serializable

type that the serialization process should execute before and after serialization and deserialization. Any

method annotated with one of these attributes must take a single System.Runtime.Serialization.

StreamingContext argument, which contains details about the source or intended destination of the

serialized object so that you can determine what to serialize. For example, you might be happy to

serialize secret data if it’s destined for another application domain in the same process, but not if the

data will be written to a file.

Table 13-1. Attributes to Customize the Serialization and Deserialization Processs



Attribute



Description



OnSerializingAttribute



Apply this attribute to a method to have it executed before the object is

serialized. This is useful if you need to modify object state before it is

serialized. For example, you may need to convert a DateTime field to UTC

time for storage.



OnSerializedAttribute



Apply this attribute to a method to have it executed after the object is

serialized. This is useful in case you need to revert the object state to what

it was before the method annotated with OnSerializingAttribute was run.



OnDeserializingAttribute



Apply this attribute to a method to have it executed before the object is

deserialized. This is useful if you need to modify the object state prior to

deserialization.



621



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



Attribute



Description



OnDeserializedAttribute



Apply this attribute to a method to have it executed after the object is

deserialized. This is useful if you need to re-create additional object state

that depends on the data that was deserialized with the object or modify

the deserialized state before the object is used.



As types evolve, you often add new member variables to support new features. This new state causes

a problem when deserializing old objects because the new member variables are not part of the

serialized object. The.NET Framework supports the attribute System.Runtime.Serialization.

OptionalFieldAttribute. When you create a new version of a type and add data members, annotate

them with OptionalFieldAttribute, and the deserialization process will not fail if they are not present.

You can then use a method annotated with OnDeserializedAttribute (see Table 13-1) to configure the

new member variables appropriately.

For the majority of custom types, the mechanisms described will be sufficient to meet your

serialization needs. If you require more control over the serialization process, you can implement the

interface ISerializable. The formatter classes use different logic when serializing and deserializing

instances of types that implement ISerializable. To implement ISerializable correctly, you must do

the following:





Declare that your type implements ISerializable







Apply the attribute SerializableAttribute to your type declaration as just

described. Do not use NonSerializedAttribute, because it will have no effect.







Implement the ISerializable.GetObjectData method (used during serialization),

which takes the argument types System.Runtime.Serialization.

SerializationInfo and System.Runtime.Serialization.StreamingContext.







Implement a nonpublic constructor (used during deserialization) that accepts the

same arguments as the GetObjectData method. Remember that if you plan to

derive classes from your serializable class, you should make the constructor

protected.



During serialization, the formatter calls the GetObjectData method and passes it SerializationInfo

and StreamingContext references as arguments. Your type must populate the SerializationInfo object

with the data you want to serialize.

If you are creating a serializable class from a base class that also implements ISerializable, your

type’s GetObjectData method and deserialization constructor must call the equivalent method and

constructor in the parent class.

The SerializationInfo class acts as a list of field/value pairs and provides the AddValue method to

let you store a field with its value. In each call to AddValue, you must specify a name for the field/value

pair; you use this name during deserialization to retrieve the value of each field. The AddValue method

has 16 overloads that allow you to add values of different data types to the SerializationInfo object.

The StreamingContext object, as described earlier, provides information about the purpose and

destination of the serialized data, allowing you to choose which data to serialize.

When a formatter deserializes an instance of your type, it calls the deserialization constructor, again

passing a SerializationInfo and a StreamingContext reference as arguments. Your type must extract the

serialized data from the SerializationInfo object using one of the SerializationInfo.Get* methods; for

example, using GetString, GetInt32, or GetBoolean. During deserialization, the StreamingContext object



622



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



provides information about the source of the serialized data, allowing you to mirror the logic you

implemented for serialization.



The Code

This following example demonstrates a serializable Employee class that implements the ISerializable

interface. In this example, the Employee class does not serialize the address field if the provided

StreamingContext object specifies that the destination of the serialized data is a file. The Main method

demonstrates the serialization and deserialization of an Employee object.

using

using

using

using

using



System;

System.IO;

System.Text;

System.Runtime.Serialization;

System.Runtime.Serialization.Formatters.Binary;



namespace Apress.VisualCSharpRecipes.Chapter13

{

[Serializable]

public class Employee : ISerializable

{

private string name;

private int age;

private string address;

// Simple Employee constructor.

public Employee(string name, int age, string address)

{

this.name = name;

this.age = age;

this.address = address;

}

// Constructor required to enable a formatter to deserialize an

// Employee object. You should declare the constructor private or at

// least protected to ensure it is not called unnecessarily.

private Employee(SerializationInfo info, StreamingContext context)

{

// Extract the name and age of the employee, which will always be

// present in the serialized data regardless of the value of the

// StreamingContext.

name = info.GetString("Name");

age = info.GetInt32("Age");

// Attempt to extract the employee's address and fail gracefully

// if it is not available.

try

{

address = info.GetString("Address");

}



623



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



catch (SerializationException)

{

address = null;

}

}

// Public property to provide access to employee's name.

public string Name

{

get { return name; }

set { name = value; }

}

// Public property to provide access to employee's age.

public int Age

{

get { return age; }

set { age = value; }

}

// Public property to provide access to employee's address.

// Uses lazy initialization to establish address because

// a deserialized object will not have an address value.

public string Address

{

get

{

if (address == null)

{

// Load the address from persistent storage.

// In this case, set it to an empty string.

address = String.Empty;

}

return address;

}

set

{

address = value;

}

}

// Declared by the ISerializable interface, the GetObjectData method

// provides the mechanism with which a formatter obtains the object

// data that it should serialize.

public void GetObjectData(SerializationInfo inf, StreamingContext con)

{

// Always serialize the employee's name and age.

inf.AddValue("Name", name);

inf.AddValue("Age", age);



624



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



// Don't serialize the employee's address if the StreamingContext

// indicates that the serialized data is to be written to a file.

if ((con.State & StreamingContextStates.File) == 0)

{

inf.AddValue("Address", address);

}

}

// Override Object.ToString to return a string representation of the

// Employee state.

public override string ToString()

{

StringBuilder str = new StringBuilder();

str.AppendFormat("Name: {0}\r\n", Name);

str.AppendFormat("Age: {0}\r\n", Age);

str.AppendFormat("Address: {0}\r\n", Address);

return str.ToString();

}

}

// A class to demonstrate the use of Employee.

public class Recipe13_01

{

public static void Main(string[] args)

{

// Create an Employee object representing Roger.

Employee roger = new Employee("Roger", 56, "London");

// Display Roger.

Console.WriteLine(roger);

// Serialize Roger specifying another application domain as the

// destination of the serialized data. All data including Roger's

// address is serialized.

Stream str = File.Create("roger.bin");

BinaryFormatter bf = new BinaryFormatter();

bf.Context =

new StreamingContext(StreamingContextStates.CrossAppDomain);

bf.Serialize(str, roger);

str.Close();

// Deserialize and display Roger.

str = File.OpenRead("roger.bin");

bf = new BinaryFormatter();

roger = (Employee)bf.Deserialize(str);

str.Close();

Console.WriteLine(roger);



625



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



// Serialize Roger specifying a file as the destination of the

// serialized data. In this case, Roger's address is not included

// in the serialized data.

str = File.Create("roger.bin");

bf = new BinaryFormatter();

bf.Context = new StreamingContext(StreamingContextStates.File);

bf.Serialize(str, roger);

str.Close();

// Deserialize and display Roger.

str = File.OpenRead("roger.bin");

bf = new BinaryFormatter();

roger = (Employee)bf.Deserialize(str);

str.Close();

Console.WriteLine(roger);

// Wait to continue.

Console.WriteLine(Environment.NewLine);

Console.WriteLine("Main method complete. Press Enter");

Console.ReadLine();

}

}

}



13-2. Implement a Cloneable Type

Problem

You need to create a custom type that provides a simple mechanism for programmers to create copies of

type instances.



Solution

Implement the System.ICloneable interface.



How It Works

When you assign one value type to another, you create a copy of the value. No link exists between the

two values—a change to one will not affect the other. However, when you assign one reference type to

another (excluding strings, which receive special treatment by the runtime), you do not create a new

copy of the reference type. Instead, both reference types refer to the same object, and changes to the

value of the object are reflected in both references. To create a true copy of a reference type, you must

clone the object to which it refers.

The ICloneable interface identifies a type as cloneable and declares the Clone method as the

mechanism through which you obtain a clone of an object. The Clone method takes no arguments, and



626



www.it-ebooks.info



CHAPTER 13 ■ COMMONLY USED INTERFACES AND PATTERNS



returns a System.Object, regardless of the implementing type. This means that once you clone an object,

you must explicitly cast the clone to the correct type.

The approach you take to implement the Clone method for a custom type depends on the data

members declared within the type. If the custom type contains only value-type data members (int, byte,

and so on) and System.String data members, you can implement the Clone method by instantiating a

new object and setting its data members to the same values as the current object. The Object class (from

which all types derive) includes the protected method MemberwiseClone, which automates this process.

If your custom type contains reference-type data members, you must decide whether your Clone

method will perform a shallow copy or a deep copy. A shallow copy means that any reference-type data

members in the clone will refer to the same objects as the equivalent reference-type data members in

the original object. A deep copy means that you must create clones of the entire object graph so that the

reference-type data members of the clone refer to physically independent copies (clones) of the objects

referenced by the original object.

A shallow copy is easy to implement using the MemberwiseClone method just described. However, a

deep copy is often what programmers expect when they first clone an object, but it’s rarely what they

get. This is especially true of the collection classes in the System.Collections namespace, which all

implement shallow copies in their Clone methods. Although it would often be useful if these collections

implemented a deep copy, there are two key reasons why types (especially generic collection classes) do

not implement deep copies:





Creating a clone of a large object graph is processor-intensive and memoryintensive.







General-purpose collections can contain wide and deep object graphs consisting

of any type of object. Creating a deep-copy implementation to cater to such

variety is not feasible because some objects in the collection might not be

cloneable, and others might contain circular references, which would send the

cloning process into an infinite loop.



For strongly typed collections in which the nature of the contained elements are understood and

controlled, a deep copy can be a very useful feature; for example, System.Xml.XmlNode implements a

deep copy in its Clone method. This allows you to create true copies of entire XML object hierarchies

with a single statement.



■ Tip If you need to clone an object that does not implement ICloneable but is serializable, you can often

serialize and then deserialize the object to achieve the same result as cloning. However, be aware that the

serialization process might not serialize all data members (as discussed in recipe 13-1). Likewise, if you create a

custom serializable type, you can potentially use the serialization process just described to perform a deep copy

within your ICloneable.Clone method implementation. To clone a serializable object, use the class

System.Runtime.Serialization.Formatters.Binary.BinaryFormatter to serialize the object to, and then

deserialize the object from a System.IO.MemoryStream object.



627



www.it-ebooks.info



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

13-1. Implement a Custom Serializable Type

Tải bản đầy đủ ngay(0 tr)

×