Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp 2005 Recipes (2006) [eng]

.pdf
Скачиваний:
48
Добавлен:
16.08.2013
Размер:
4.04 Mб
Скачать

458 C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

Solution

For serialization of simple types, apply the attribute System.SerializableAttribute to the type declaration. For types that are more complex, or to control the content and structure of the serialized data, implement the interface System.Runtime.Serialization.ISerializable.

How It Works

Recipe 2-13 showed how to serialize and deserialize an object using the formatter classes provided with the .NET Framework class library. However, types are not serializable by default. To implement a custom type that is serializable, you must apply the attribute SerializableAttribute to your type declaration. As long as all of the data fields in your type are serializable types, applying SerializableAttribute is all you need to do to make your custom type serializable. If you are implementing a custom class that derives from a base class, the base class must also be serializable.

Caution Classes that derive from a serializable type don’t inherit the attribute SerializableAttribute. To make derived types serializable, you must explicitly declare them as serializable by applying the

SerializableAttribute attribute.

Each formatter class contains the logic necessary to serialize types decorated with SerializableAttribute and will correctly serialize all public, protected, and private fields. You can exclude specific fields from serialization by applying the attribute System.NonSerializedAttribute to those fields. As a rule, you should exclude the following fields from serialization:

Fields that contain nonserializable 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 the data is large

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.

C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

459

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.

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. .NET Framework 2.0 introduces 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.

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.

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. 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.

460 C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

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 provides information about the source of the serialized data, allowing you to mirror the logic you implemented for serialization.

Note During standard serialization operations, the formatters do not use the capabilities of the StreamingContext object to provide specifics about the source, destination, and purpose of serialized data. However, if you wish to perform customized serialization, your code can configure the formatter’s StreamingContext object prior to initiating serialization and deserialization. Consult the .NET Framework SDK documentation for details of the

StreamingContext class.

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 System; using System.IO; using System.Text;

using System.Runtime.Serialization;

using 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.

C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

461

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");

}

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.

462 C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

inf.AddValue("Name", name); inf.AddValue("Age", age);

//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);

//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");

C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

463

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 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 implementing the Clone method for a custom type depends on the data members declared within the type. If the custom type contains only value-type (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 referencetype data members in the clone will refer to the same objects as the equivalent reference-type data

464C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

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 memory-intensive.

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, the 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.

The Code

The following example demonstrates various approaches to cloning. The simple class named Employee contains only string and int members, and so relies on the inherited MemberwiseClone method to create a clone. The Team class contains an implementation of the Clone method that performs a deep copy. The Team class contains a collection of Employee objects, representing a team of people. When you call the Clone method of a Team object, the method creates a clone of every contained Employee object and adds it to the cloned Team object. The Team class provides a private constructor to simplify the code in the Clone method. The use of constructors is a common approach to simplify the cloning process.

using System; using System.Text;

using System.Collections.Generic;

namespace Apress.VisualCSharpRecipes.Chapter13

{

public class Employee : ICloneable

{

public string Name; public string Title; public int Age;

C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

465

// Simple Employee constructor.

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

{

Name = name; Title = title; Age = age;

}

//Create a clone using the Object.MemberwiseClone method because the

//Employee class contains only string and value types.

public object Clone()

{

return MemberwiseClone();

}

// Returns a string representation of the Employee object. public override string ToString()

{

return string.Format("{0} ({1}) - Age {2}", Name, Title, Age);

}

}

public class Team : ICloneable

{

// A List to hold the Employee team members. public List<Employee> TeamMembers =

new List<Employee>();

public Team()

{

}

//Private constructor called by the Clone method to create a new Team

//object and populate its List with clones of Employee objects from

//a provided List.

private Team(List<Employee> members)

{

foreach (Employee e in members)

{

//Clone the individual employee objects and

//add them to the List. TeamMembers.Add((Employee)e.Clone());

}

}

//Adds an Employee object to the Team. public void AddMember(Employee member)

{

TeamMembers.Add(member);

}

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

//entire Team.

public override string ToString()

{

StringBuilder str = new StringBuilder();

466 C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

foreach (Employee e in TeamMembers)

{

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

}

return str.ToString();

}

// Implementation of ICloneable.Clone. public object Clone()

{

//Create a deep copy of the team by calling the private Team

//constructor and passing the ArrayList containing team members. return new Team(this.TeamMembers);

//The following command would create a shallow copy of the Team.

//return MemberwiseClone();

}

}

// A class to demonstrate the use of Employee. Public class Recipe13_02

{

public static void Main()

{

//Create the original team. Team team = new Team();

team.AddMember(new Employee("Frank", "Developer", 34)); team.AddMember(new Employee("Kathy", "Tester", 78)); team.AddMember(new Employee("Chris", "Support", 18));

//Clone the original team.

Team clone = (Team)team.Clone();

//Display the original team. Console.WriteLine("Original Team:"); Console.WriteLine(team);

//Display the cloned team. Console.WriteLine("Clone Team:"); Console.WriteLine(clone);

//Make change.

Console.WriteLine("*** Make a change to original team ***");

Console.WriteLine(Environment.NewLine); team.TeamMembers[0].Name = "Luke"; team.TeamMembers[0].Title = "Manager"; team.TeamMembers[0].Age = 44;

//Display the original team. Console.WriteLine("Original Team:"); Console.WriteLine(team);

//Display the cloned team. Console.WriteLine("Clone Team:"); Console.WriteLine(clone);

C H A P T E R 1 3 C O M M O N LY U S E D I N T E R FA C E S A N D PAT T E R N S

467

// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine();

}

}

}

13-3. Implement a Comparable Type

Problem

You need to provide a mechanism that allows you to compare custom types, enabling you to easily sort collections containing instances of those types.

Solution

To provide a standard comparison mechanism for a type, implement the generic System.IComparable<T> interface. To support the comparison of a type based on more than one characteristic, create separate types that implement the generic System.Collections.Generic.IComparer<T> interface.

Caution The System.IComparable and System.Collections.IComparer interfaces available prior to .NET Framework 2.0 do not use generics to ensure type safety. When working with .NET Framework 1.0 or 1.1, you must take extra precautions to ensure the objects passed to the methods of these interfaces are of the appropriate type.

How It Works

If you need to sort your type into only a single order, such as ascending ID number, or alphabetically based on surname, you should implement the IComparable<T> interface. IComparable<T> defines

a single method named CompareTo, shown here.

int CompareTo(T other);

According to the specification of the CompareTo method, the object (other) passed to the method must be an object of the same type as that being called, or CompareTo must throw a System. ArgumentException exception. This is less important in .NET Framework 2.0, given that the implementation of IComparable uses generics and is type-safe, ensuring that the argument is of the correct type. The value returned by CompareTo should be calculated as follows:

If the current object is less than other, return less than zero (for example, –1).

If the current object has the same value as other, return zero.

If the current object is greater than other, return greater than zero (for example, 1).

What these comparisons mean depends on the type implementing the IComparable interface. For example, if you were sorting people based on their surname, you would do a String comparison on this field. However, if you wanted to sort by birthday, you would need to perform a comparison of the corresponding System.DateTime fields.

Соседние файлы в предмете Программирование на C++