Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Object Serialization in .NET


October, 2004: Object Serialization In .NET

How to customize .NET serialization

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at richardrichardgrimes.com.


Object serialization forms an important part of the .NET Framework. It is vital to web services and SOAP, to .NET Remoting, and to Application Domains and .NET Contexts. Object serialization is also an important facility for user code. It is useful to make an object persistent in a file on your machine so that you can load it at a later time, to make it persistent in a database so that it can be shared with other users in the enterprise, or make it persistent in XML and share it with other users over the Internet. In this article, I outline how objects are serialized and how to customize the mechanism.

Serializing Objects

There are two principle features to an object—its state and its behavior. An object's behavior is defined by code, and in .NET this means the assembly that defines the class from which the object is created. Serialization is only concerned with the state of an object, and it is important to note that in most cases, a serialized object is useless without access to the assembly that defines the object's class. .NET serialization recognizes this and ensures that information about the assembly is stored along with the object's state. The rationale here is that only an instance of a specific class will be able to recognize and use the state serialized from another instance of that class. However, it is possible to change this mechanism and allow the data to be used to initialize an object of a different class.

Serialization takes the object's state and converts it into a series of bytes. .NET provides a generic mechanism for handling a series of bytes called ".NET streams." The Framework provides several standard stream types designed to read/write bytes from various sources: files, network sockets, and memory. The stream paradigm lets you "chain" together streams. The Framework provides the CryptoStream class that can be created from, and so chained to, another stream. This means that an object can be serialized to a file, a socket or memory, using the same code for each, and it can even be encrypted as it is serialized.

The object that performs serialization is called a "formatter," and .NET provides two formatter classes:

  • BinaryFormatter serializes the raw state of the object.
  • SoapFormatter converts the state to the SOAP schema XML.

A serializable class is marked with the [Serializable] pseudocustom attribute (pseudo, because it changes the class's metadata directly through the serializable class attribute, it does not add custom metadata with the custom directive). The [Serializable] attribute is not inherited by a derived class because that child class may have members that make it not serializable. If the child class is serializable, then it, too, must be marked with the [Serializable] attribute. A formatter serializes all the fields of a serializable object, including nonpublic fields. The formatter will not access properties because properties are access methods.

If you do not want a field serialized, then you can apply the [NonSerialized] pseudocustom attribute. This means that when the object is deserialized, such fields will not be initialized. The Framework provides the interface IDeserializationCallback and formatters specifically look for this interface on the objects it is deserializing. If the object implements this interface, the formatter calls the member method, OnDeserialization, when it has finished its work to give the object the opportunity to initialize any fields that were not initialized by the formatter.

Custom Serialization

In some cases you will want to serialize data that is not serializable. For example, a FileStream object is essentially a wrapper around the Win32 file API, and you can see this because the class has a Handle property, which is the Win32 HANDLE to the file that has been identified elsewhere by its name and path. This handle also gives access to the current position within the file. However, the handle is an opaque 32-bit integer that has no meaning to other processes. It makes no sense to serialize this value, and so the FileStream class does not have the [Serializable] attribute. However, a FileStream object is simply a position within a file, so the important information in a FileStream object are the name and path of the file and the position within the file. One way to serialize a FileStream object would be to ensure that this information is put into the serialization stream.

.NET provides a mechanism—called "custom serialization"—to customize what is put into the serialization stream. A class that supports custom serialization implements an interface called ISerializable (Example 1), which has a single method called GetObjectData. This method is called by a formatter to extract the state of the serializable object. There is no method to initialize the object using a serialized state because that is the job of constructors and an interface cannot contain a constructor. Any class that implements ISerializable should also implement a constructor with the same parameters as GetObjectData, and since this constructor is only called by a formatter, which has access to nonpublic members of any class, it makes sense to implement this special constructor as a protected member.

A class that uses custom serialization must also be marked with the [Serializable] attribute, but the current .NET compilers do not check for this, nor do they check that the appropriate constructor is implemented. Such errors only come to light at runtime when an exception will be thrown during serialization. Listing One shows an example of a class that uses custom serialization.

During serialization, the formatter calls GetObjectData passing a SerializationInfo object that is essentially a collection of data. GetObjectData can call this object to add appropriate data to the object. In Listing One, the method adds the name of the file and the file position to the collection. When the object is deserialized, the formatter creates a SerializationInfo object from information in the serialization stream and passes this to the constructor. Listing One shows this, but notice that, because I have no information about whether the original object still has a reference to the FileStream object, I have to ensure that both the original object and the deserialized object have shared access to the same file (this is the purpose of the OpenFile method).

The formatter also passes information about the type of serialization being performed through the StreamingContext parameter. This type has two members, one is called Context and is for your own use, it is neither set nor used by the formatter. The other is called State and is a StreamingContextStates enumeration. State gives an indication of how the serialization occurs; for example, whether it is performed to pass an object by value across application domain boundaries in the same process, between processes on the same machine or between machines. The serialization code can use this value to determine what data to add to the serialization stream, and the deserialization code can interpret the data in the stream in a different way, depending on the context. This value is set by the formatter object. So if the object is passed by value between Application domains, .NET Remoting will set this value. If you create the formatter yourself, you can set this value through the formatter's IFormatter.Context property. It is usually a good idea to do this because the default value for the State member is StreamingContextStates.All, which gives little information. Listing Two is an example of doing this.

Deserializing to a Different Type

When an object is serialized, the type of object and the name of the assembly that defines the type are written to the serialization stream. The default serialization mechanism reads this type and assembly name, loads the assembly, and creates an instance of the specified type before initializing the new object with data from the stream. However, the serialization mechanism gives various ways to customize how the formatter determines what type will be created.

The SerializationInfo object passed to GetDataObject during serialization contains two read/write properties that can be used to alter the type that is deserialized. FullTypeName is the name of the type (including the namespace of the type) and AssemblyName is the full name of the assembly that contains the type. This is the information that is put into the serialization stream. The GetDataObject method of your custom serialized object can change these properties and, of course, you can take the serialization streaming context into account, as in Listing Three.

Another way to change the deserialized type is through the Binder object that is part of the formatter. The Binder property is an instance of the SerializationBinder class. This object is called during deserialization and is given an opportunity to change the type of the deserialized object. This is performed in the BindToType method, which is passed the name of the type and assembly that was put in the serialization stream. This method can then perform some logic to determine the actual type that should be used and return the type object from the method. Listing Four is a binder class and Listing Five shows using this class in action. Note that there is a bug in Version 1.1 of the Framework. If the object is not a member of a namespace (which is a perfectly legitimate case), the name of the class is prefixed with a period, so in Listing Four, I have to take this into account when comparing names.

The binder performs the check when the object is deserialized, but it does not have access to the serialization stream or any information about it like the serialization context. However, you could provide such information to the binder object constructor when you call it.

Serializing a Nonserializable Object

The final way to change to customize serialization is to use what is known as a "surrogate." Again, you specify this through the formatter object, but the surrogate can be used for serialization and deserialization. As the name suggests, a surrogate is an object used in place of another. A surrogate, for example, can be used to serialize an object that does not have the [Serializable] attribute and hence should not be serializable.

There are two steps needed to create a surrogate. The first step is to create the surrogate class by implementing the ISerializationSurrogate interface. This interface has two methods, GetObjectData and SetObjectData, which are used to get the state of the object being serialized and to initialize an object with data from the serialization stream. Since it is a surrogate, it means that the object that is being serialized is passed to GetObjectData through a parameter, and the object that the formatter wants initialized is passed to SetObjectData. SetObjectData returns an object, so potentially this method could return a new object of a different type, but the formatters in the current version of the Framework ignore this return value, so at the moment the deserialized object is the type specified in the serialization stream.

Your code does not have any special access to either the object being serialized or to the object being deserialized, so you will only be able to access public members. You can use reflection to get around this problem (assuming your code has reflection code-access security permissions) but this means hacking into class implementations, and there is no guarantee that the surrogate will work with a future version of the class. This is illustrated in Listing Six. The Handle and Name properties of the FileStream class are read only so although you can read them in the surrogate's GetObjectData method you cannot write to them in the SetObjectData method; instead, you will have to access the underlying private fields (_handleProtector and _fileName) and set them with appropriate values. The _fileName field can be initialized with the value read from the Name property, but the _handleProtector field is more tricky because it is an instance of a private type. To initialize this requires using reflection to access the type and its constructor and then invoke this constructor. Listing Six illustrates this but is not a robust solution because it assumes that the code serializing and deserializing the file object will have shared access to the underlying file.

The second step to using a surrogate is to register it with the formatter object. The formatter has a SurrogateSelector property that is used to hold a reference to an instance of the SurrogateSelector class. A SurrogateSelector is essentially a collection of surrogate objects that the formatter consults to get the surrogate for a specified type. Listing Seven shows how to add a surrogate to a SurrogateSelector and, as you can see, the surrogate object is associated with a streaming context and the type of object that it will act as a serialization surrogate. The deserialization code also needs to register a surrogate, which potentially could be different to the one used to serialize the object. In addition, each surrogate selector is one object in a chain of selectors. You can add additional selectors through the ChainSelector method implemented by all selectors. A reference to the next selector in the chain is passed as the last parameter of SetObjectData.

Examples of Serializable Objects

The Framework contains many serializable objects and it is interesting to take a look at two of them. The first example is the Exception class. This class is custom serializable. Exception objects can be serialized because of the .NET contexts architecture. When an object is accessed across a context boundary, .NET remoting serializes the method calls. If a method throws an exception, the exception object is passed across the context boundary by value; that is, the exception object is serialized and the bytes in the stream are passed across the context boundary where they can be deserialized into the exception object.

You can use this facility in your code. An exception object contains a lot of information: the type of the exception, the description of the error condition, the source assembly, an HRESULT, a URL to any additional help, and an inner exception (that is, a reference to another exception object) if it exists. In addition, when an exception is caught, the runtime adds a stack trace to the exception object. Your exception-handling code can serialize the exception and store it in some persistent storage, like a database, so that your postmortem analysis of a faulted application will have complete information about the exception.

If the exception originated in an object in another context (for example, a remote object on another machine), then the exception that your code catches will contain a dump of the exception that was thrown in the remote context (in the RemoteStackTraceString item in the SerializationInfo collection) as well as the information contained in the exception thrown in the current context. However, there is a hidden problem here. The [Serializable] attribute is not inherited by a derived class, so if you derive your own exception types, and the code that throws these exceptions can be accessed across context boundaries, then you must take steps to make sure that the new exception is serializable.

To do this, your new exception class must implement the constructor that you would normally implement if you implement the ISerializable interface, even if your new class does not add any fields. The reason is that a formatter will look for the appropriate constructor on an exception class that it will use to create the object from the serialization stream. If this constructor is not found, the formatter throws a SerializationException exception. .NET remoting uses a formatter to return values from a method call, so you will get a surprise when your method call returns a SerializationException, which is apparently unrelated to the possible exceptions that the remote object can throw.

So if you implement a new exception type, you must implement a serialization constructor and this constructor must call the base class version. If your new exception type adds fields then you should also implement the ISerializable interface so that you can serialize those fields; the GetObjectData method must also call the base class version so the fields of the base object will also be serialized. Listing Eight gives a summary of this.

The second example I want to mention is the Delegate class. A delegate is effectively a managed function pointer so that the delegate object holds information about the method to call and the specific object instance where it should be called. The Delegate class is abstract but it is custom serializable. That is, it implements ISerializable.GetObjectData but does not implement the corresponding constructor because instances of the class cannot be created. Instead, when you use a delegate in your code, you will use an instance of MulticastDelegate, which is derived from Delegate. A multicast delegate holds a linked list that contains one or more delegates; thus, when the multicast delegate is invoked, the linked list is walked and the delegates are invoked serially.

.NET remoting lets you provide callbacks between contexts through delegates, and to do this, a delegate has to be passed by value and, hence, the delegate must be serializable. The Delegate class uses an internal class called DelegateSerializationHolder to serialize information about the delegate object. This internal class serializes information about the target objects held in the delegate and the methods to be invoked. If a delegate has a target object (that is, the method invoked is not static), the class of the target object must be marshal by reference or serializable. If the target object is marshal by reference, the reference will be passed to the context where the delegate is invoked, which means that the method will be invoked in the context where the target object was created. Clearly, this requires that the target object is available when the delegate is invoked.

If the target object class is serializable, the object is serialized when the delegate is serialized and both are put in the serialization stream. This means that the target object will be deserialized in the same context as the delegate and so the method will be executed in the same context as where the delegate is invoked. Since all of the information about the delegate and how it is invoked is stored in the stream, this stream can be saved in a persistent store and deserialized and invoked at a later time. Given the delegate and serializable class in Listing Nine, the application in Listing Ten can be called to serialize the delegate to a file and then the process shuts down. At some other time (and on a different machine if the process, library, and the data file data.xml are copied there), another instance of the process can be run to deserialize and invoke the delegate.

Conclusion

.NET object serialization is flexible and powerful and it is used extensively by the .NET Framework. The mechanism provides several ways to customize how data is serialized. You have the choice of how the serialized state is represented through the type of formatter used. You have further control about the data that is serialized by implementing the ISerializable interface on the serialized object. Normally, an object is deserialized to the same type of object that was serialized, but you can customize this either by changing the AssemblyName and FullTypeName properties of the SerializationInfo object, when the formatter calls your object's GetObjectData method, or by providing a Binder object to the formatter that deserializes the object from the stream. You can further customize the serialization and deserialization mechanism through a surrogate object but, at present, this will only deserialize to an object of the same type that was serialized. The final point is that if you derive from a class that is serializable, there must be an important reason why the base class was serializable so you should take steps to make the derived class serializable, too, unless you have a strong reason not to.

DDJ



Listing One

[Serializable]
public class SerializableFile : ISerializable, IDisposable
{
   // Constructor only used for deserialization
   protected SerializableFile(
      SerializationInfo info, StreamingContext context)
   {
      OpenFile( info.GetString("name"), info.GetInt64("position"));
   }
   // Method provides state for serialization
   public void GetObjectData(
      SerializationInfo info, StreamingContext context)
   {
      if (file != null)
      {
         info.AddValue("name", file.Name);
         info.AddValue("position", file.Position);
      }
   }
   private Stream file;
   // Constructor called by user code
   public SerializableFile(string name)
   {
      OpenFile(name, 0);
   }
   // Provide deterministic clean up
   public void Dispose()
   {
      if (file != null) file.Close();
      file = null;
   }
   // Other methods that use file
   // Helper method to initialise object
   private void OpenFile(string name, long position)
   {
      file = new FileStream(name, FileMode.OpenOrCreate, 
                   FileAccess.ReadWrite, FileShare.ReadWrite);
      file.Position = position;
   }
}
Back to article


Listing Two
FileStream stm = new FileStream( 
          "data.soap", FileMode.OpenOrCreate, FileAccess.Write);
SoapFormatter sf = new SoapFormatter();
sf.Context = new StreamingContext(StreamingContextStates.File);
// obj is some serializable object
sf.Serialize(stm, obj);
stm.Close();
Back to article


Listing Three
[Serializable]
class SerializedObject : ISerializable
{
   public void GetObjectData(
      SerializationInfo info, StreamingContext context)
   {
      switch(context.State)
      {
     case StreamingContextStates.CrossMachine:
         info.FullTypeName = "OtherMachineObject";
         break;
     case StreamingContextStates.Process:
         info.FullTypeName = "OtherProcessObject";
         break;
      // All other cases will be deserialized as SerializedObject
      }
      // Add values
   }
   // Other members
}
Back to article


Listing Four
class MyBinder : SerializationBinder
{
   public override Type BindToType(string assemName, string typeName)
   {
      Type type = null;
      String myName = Assembly.GetExecutingAssembly().FullName;
      // If the serialized class is the first version, replace it with the 
      // second version of the class. Note the preceding period in class name.
      if (typeName == ".ClassV1" && assemName == myName)
      {
         type = Type.GetType(String.Format("ClassV2, {0}", myName));
      }
      else
      {
         type = Type.GetType(String.Format("{0}, {1}", typeName, assemName));
      }
      return type;
   }
}
Back to article


Listing Five
FileStream stm = new FileStream( "data.soap", FileMode.Open, FileAccess.Read);
SoapFormatter sf = new SoapFormatter();
sf.Binder = new MyBinder();
ClassV2 o = (ClassV2)sf.Deserialize(stm);
stm.Close();
// Use the object
Back to article


Listing Six
// Example of a serialization surrogate. This is a contrived example used to 
// indicate that surrogates are not a solution for every situation.
class FileSurrogate : ISerializationSurrogate
{
   // Platform Invoke access to the Win32 file API
   [DllImport("kernel32")]
   static extern int CreateFile(string name, uint access, 
      uint share, uint security, uint creation, uint attr, uint template);
   const uint GENERIC_READWRITE = 0xc0000000;
   const uint FILE_SHARE_READWRITE = 3;
   const uint OPEN_ALWAYS = 4;
   // Open or create a file for read/write, shared for read and write access
   static int CreateFile(string name)
   {
      return CreateFile(name, GENERIC_READWRITE, 
         FILE_SHARE_READWRITE, 0, OPEN_ALWAYS, 0, 0);
   }
   // Serialize the state of the object that is important for
   // recreating a FileStream object
   public void GetObjectData(object obj, SerializationInfo info,
      StreamingContext context)
   {
      FileStream fs = obj as FileStream;
      info.AddValue("name", fs.Name);
      info.AddValue("position", fs.Position);
      info.AddValue("canread", fs.CanRead);
      info.AddValue("canwrite", fs.CanWrite);
      info.AddValue("canseek", fs.CanSeek);
      // This is not accessible through a property, so use reflection
      FieldInfo fi = fs.GetType().GetField("_bufferSize", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      info.AddValue("buffersize", (int)fi.GetValue(fs));
   }
   // Use the serialization stream to initialize an already created object
   public object SetObjectData(object obj, SerializationInfo info, 
      StreamingContext context, ISurrogateSelector selector)
   {
      FileStream fs = obj as FileStream;
      // Use Platform Invoke to open the file, note that we 
      // assume read and write access to the file is shared
      int ihandle = CreateFile(info.GetString("name"));
      IntPtr handle = new IntPtr(ihandle);
      // The handle is ref count protected in the FileStream object,
      // so create a __FileStreamHandleProtector class to do this
      Type type = fs.GetType().GetNestedType(
         "__FileStreamHandleProtector", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      if (type != null)
      {
         // Now create the __FileStreamHandleProtector object using the handle
         ConstructorInfo ci;
         ci = type.GetConstructor( 
                  BindingFlags.Instance |BindingFlags.NonPublic 
               |  BindingFlags.CreateInstance,
            null, new Type[]{typeof(IntPtr), typeof(Boolean)}, null);
         object o = ci.Invoke(new object[]{handle, true});
         if (o != null)
         {
            // Initialize FileStream._handleProtector in object we were given
            FieldInfo fihp = fs.GetType().GetField("_handleProtector", 
               BindingFlags.Instance|BindingFlags.NonPublic);
            fihp.SetValue(fs, o);
         }
      }
      // Set the other FileStream fields
      FieldInfo fi = fs.GetType().GetField("_canRead", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      fi.SetValue(fs, info.GetBoolean("canread"));
      fi = fs.GetType().GetField("_canWrite", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      fi.SetValue(fs, info.GetBoolean("canwrite"));
      fi = fs.GetType().GetField("_canSeek", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      fi.SetValue(fs, info.GetBoolean("canwrite"));
      fi = fs.GetType().GetField("_bufferSize", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      fi.SetValue(fs, info.GetInt32("buffersize"));
      fi = fs.GetType().GetField("_fileName", 
         BindingFlags.Instance|BindingFlags.NonPublic);
      fi.SetValue(fs, info.GetString("name"));
      // This is the only field we can write through a property
      fs.Position = info.GetInt64("position");
      return null; // Formatters in the current versions of 
   }               // .NET ignore this return value.
}
Back to article


Listing Seven
// Initialize the formatter's surrogate selector with our surrogate
SoapFormatter sf = new SoapFormatter();
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(
   typeof(FileStream), new StreamingContext(StreamingContextStates.All),
   new FileSurrogate());
sf.SurrogateSelector = ss;
// This is the object we will serialize
FileStream fs = new FileStream("data.txt", FileMode.OpenOrCreate, 
   FileAccess.ReadWrite, FileShare.ReadWrite);
// Write some data 
using (StreamWriter sw = new StreamWriter(fs))
{
   sw.WriteLine("first line");
   sw.WriteLine("second line");
   // We will serialize it to this stream
   using (FileStream stm = new FileStream("data.soap",
      FileMode.OpenOrCreate, FileAccess.Write))
   {
      // Serialize the object
      sf.Serialize(stm, fs);
   } // Note that stm is close here, this is important
} // The StreamWriter and FileStream are closed here
Back to article


Listing Eight
// Every new exception class should be serializable because
// it could be passed across context boundaries
[Serializable]
class MyException : Exception, ISerializable
{
   // At a minimum, this constructor must be provided
   protected MyException(
      SerializationInfo info, StreamingContext context) 
      // The base class version should be called
      : base(info, context)
   {
      // If exception has additional fields, they should be deserialized here
      data = info.GetString("Data");
   }
   // If your exception has additional fields you must override
   // this method and use it to serialize your fields
   public override void GetObjectData(
      SerializationInfo info, StreamingContext context)
   {
      info.AddValue("Data", data);
      // You must call the base class version
      base.GetObjectData(info, context);
   }
   // The following are members that this class adds
   public MyException(string message, string d) : base(message)
   {
      data = d;
   }
   string data;
   public string Data {get{return data;}}
}
Back to article


Listing Nine
public delegate void Del();
// This is the class that will be invoked. 
[Serializable]
public class X
{
   private string str;
   public X(string s)
   {
      str = s;
   }
   // The method that will be invoked through the serialized delegate
   public void CallMe()
   {
      Console.WriteLine("you called {0}", str);
   }
}
Back to article


Listing Ten
class App
{
   static void Main()
   {
      if (Environment.GetCommandLineArgs().Length == 1)
      {
         Usage();
         return;
      }
      if (Environment.GetCommandLineArgs()[1].Equals("serial"))
      {
         SerializeDelegate();
      }
      else if (Environment.GetCommandLineArgs()[1].Equals("deserial"))
      {
         Deserialize();
      }
      else Usage();
   }
   static void Usage()
   {
      Console.WriteLine("Usage: SerDel <command>");
      Console.WriteLine("where <command> is serial or deserial");
   }
   // The user has requested that a delegate should be serialized
   static void SerializeDelegate()
   {
      Console.WriteLine("The time is {0}", DateTime.Now.ToString());
      Console.WriteLine("Serializing to data.xml...");
      // Store the time that the delegates were created
      string s;
      s = String.Concat("First object created ",
             DateTime.Now.ToString());
      X x1 = new X(s);
      Del d = new Del(x1.CallMe);
      Console.WriteLine(s);
      // Add a delay so that the times are suitably different
      Thread.Sleep(1000);
      // Add second deleg to show that multicast delegates can be serialized
      s = String.Concat("Second object created ", DateTime.Now.ToString());
      X x2 = new X(s);
      d += new Del(x2.CallMe);
      Console.WriteLine(s);
      // Serialize the delegate. Because I have used the SOAP
      // formatter, you can view this file to see the details
      // of how the delegate is serialized
      using (FileStream fs = File.Open("data.xml", FileMode.Create))
      {
         SoapFormatter sf = new SoapFormatter();
         sf.Serialize(fs, d);
      }
   }
   // The user has indicated that the stored delegate should be invoked. 
   // This will be a different process to the one that created the delegate. 
   // Indeed, you can copy serdel.exe, objs.dll and data.xml to a 
   // different machine and invoke the delegate there...
   static void Deserialize()
   {     
      Console.WriteLine("The time is {0}", DateTime.Now.ToString());
      Console.WriteLine("Deserializing...");
      using (FileStream fs = File.Open("data.xml", FileMode.Open))
      {
         SoapFormatter sf = new SoapFormatter();
         Del d = (Del)sf.Deserialize(fs);
         // Invoke the delegate
         d();
      }
   }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.