.NET implements the automatic serialization of objects by means of reflection, a simple and elegant technique that uses metadata exposed by every .NET component. .NET can capture the value of every one of an object's fields and
serialize it to memory, to a file, or to a network connection. .NET also
supports automatic deserialization: .NET can create a new object, read
its persisted field values, and, using reflection, set the values of its
fields. Because reflection can access private fields, including
base-class fields, .NET can take a complete snapshot of the state of an
object during serialization and perfectly reconstruct that state during
deserialization. Another advantage of reflection-based serialization is
that the code used by .NET is completely general-purpose—the state of
every .NET type can be read or set using reflection.
.NET serializes the object state into a stream. A stream
is a logical sequence of bytes, independent of any particular medium
(file, memory, communication port, or other resource). This extra level
of indirection means that you can use the same serialization
infrastructure with any medium, simply by selecting an appropriate
stream type. The various stream types provided by .NET all derive from
the abstract class Stream, defined in the System.IO namespace. Although you need a Stream
instance to serialize and deserialize an object, there is usually no
need to interact explicitly with the methods and properties of the Stream itself.
.NET serialization is object-based. As a result,
only instance fields are serialized. Static fields are excluded from
serialization. |
|
1. The Serializable Attribute
By default, user-defined types (classes and
structs) aren't serializable. The reason is that .NET has no way of
knowing whether a reflection-based dump of the object state to a stream
makes sense. Perhaps the object members have some transient value or
state (such as an open database connection or communication port). If
.NET simply serialized the state of such an object, when you constructed
a new object by deserializing it from the stream you would end up with a
defective object. Consequently, serialization can be performed only
with the developer's consent.
Enumerations are always serializable. |
|
To indicate to .NET that instances of your class are serializable, you can add the Serializable attribute to your class definition. For example:
[Serializable]
public class MyClass
{
public string SomeString;
public int SomePublicNumber;
int m_SomePrivateNumber;
/* Methods and properties */
}
In most cases, decorating a user-defined type definition with the Serializable
attribute is all you need to do. If the class has member variables that
are complex types themselves, .NET automatically serializes and
deserializes these members as well:
[Serializable]
public class MyOtherClass
{...}
[Serializable]
public class MyClass
{
MyOtherClass m_Obj;
/* Methods and properties */
}
The result is recursive iteration over an object
and all its contained objects. The object can be the root of a huge
graph of interconnected objects, as shown in Figure 1.
Regardless of its depth, .NET captures the entire
state of any graph and serializes it. The recursive traversal algorithm
used by .NET is smart enough to detect cyclic references in the graph,
tagging objects it has already visited and thereby avoiding processing
the same object twice. This approach allows .NET to serialize complex
data structures such as doubly linked lists.
2. Non-Serializable Members
When a class is serializable, .NET insists that
all its member variables be serializable as well. If it discovers a
non-serializable member, it throws an exception of type SerializationException
during serialization. However, what if the class or a struct has a
member that can't be serialized? That type will not have the Serializable
attribute and will preclude the containing type from being serialized.
Commonly, that non-serializable member is a reference type that requires
some special initialization. The solution to this problem requires
marking such a member as non-serializable and taking a custom step to
initialize it during deserialization.
To allow a serializable type to contain a non-serializable type as a member variable, you need to mark the member with the NonSerialized field attribute:
public class MyOtherClass
{...}
[Serializable]
public class MyClass
{
[NonSerialized]
MyOtherClass m_Obj;
/* Methods and properties */
}
When .NET serializes a member variable, it first reflects it to see whether it has the NonSerialized
attribute. If so, .NET ignores that variable and simply skips over it.
This allows you to preclude from serialization even serializable types:
[Serializable]
public class MyClass
{
[NonSerialized]
int m_Number;
}
However, when .NET deserializes the object, it
initializes each non-serializable member variable to the default value
for that type (null for all reference types). It's then up to
you to provide code to initialize the variables to their correct values.
To that end, the object needs to know when it's being deserialized. The
notification takes place by implementing the interface IDeserializationCallback, defined in the System.Runtime.Serialization namespace:
public interface IDeserializationCallback
{
void OnDeserialization(object sender);
}
IDeserializationCallback's single method, OnDeserialization( ), is called after .NET has deserialized the object, allowing it to perform the required custom initialization steps. The sender parameter is ignored and is always set to null by .NET. Example 1 demonstrates how you can implement IDeserializationCallback. In the example, the class MyClass has a database connection as a member variable. The connection object (SqlConnection) isn't a serializable type and so is marked with the NonSerialized attribute. MyClass creates a new connection object in its implementation of OnDeserialization( ), because after deserialization the connection member is set to its default value of null. MyClass then initializes the connection object by providing it with a connection string and opens it.
Example 1. Deserialized event using IDeserializationCallback
using System.Runtime.Serialization;
[Serializable]
public class MyClass : IDeserializationCallback
{
[NonSerialized]
IDbConnection m_Connection;
string m_ConnectionString;
public void OnDeserialization(object sender)
{
Debug.Assert(m_Connection == null);
m_Connection = new SqlConnection( );
m_Connection.ConnectionString = m_ConnectionString;
m_Connection.Open( );
}
/* Other members */
}
|
You can't initialize class members marked with the readonly directive in OnDeserialization( )—such
members can only be initialized in a constructor.
|
|
On non-sealed classes, it is important to use either implicit interface implementation of IDeserializationCallback and have the base class mark its OnDeserialization( ) method as virtual to allow subclasses to override it. When class
hierarchies are involved, you need to call your base-class
implementation of OnDeserialization( ), and that requires a non-private implementation:
[Serializable]
public class MyBaseClass : IDeserializationCallback
{
public virtual void OnDeserialization(object sender)
{...}
}
[Serializable]
public class MySubClass : MyBaseClass
{
public override void OnDeserialization(object sender)
{
//Perform custom steps, then:
base. OnDeserialization(sender);
}
}
2.1. Delegates and serialization
All delegate definitions are compiled into
serializable classes. This means that when you serialize an object that
has a delegate member variable, the internal invocation list of the
delegate is serialized too. I believe that this renders delegates
inherently non-serializable. There are no guarantees that the target
objects in the internal list are serializable, so sometimes the
serialization will work and sometimes it will throw a serialization
exception. In addition, the object containing the delegate typically
does not know or care about the actual state of the delegate. This is
especially true when the delegate is used to manage event subscriptions,
because the exact number and identities of the subscribers are often
transient values that should not be persisted between application
sessions.
You should mark delegate member variables as non-serializable, using the NonSerialized attribute:
[Serializable]
public class MyClass
{
[NonSerialized]
EventHandler m_MyEvent;
}
In the case of events, you must also add the field attribute qualifier when applying the NonSerialized attribute:
[Serializable]
public class MyPublisher
{
[field: NonSerialized]
public event EventHandler MyEvent;
}