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

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

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

298 C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

Table 8-11. Common StreamWriter Members

Method

Description

Close()

Closes the file and releases any resources

Flush()

Forces the writing of the current buffer and then clears it

Write()

Writes the specified String to the output stream

WriteLine()

Writes the specified String to the output stream, then writes the NewLine String

 

 

There are many ways to create a StreamReader and a StreamWriter. You can start from the File or FileInfo class and create one directly from its methods. It is also possible to build one from a FileStream, again using the File or FileInfo class or with the FileStream constructor.

StreamReader ^sr1 = File::OpenText("file.dat");

StreamWriter ^sw1 = fileinfo->CreateText("file.dat");

StreamReader ^sr2 = gcnew StreamReader(File::Open("file.dat", FileMode::Open, FileAccess::Read, FileShare::None));

StreamWriter ^sw2 = gcnew StreamWriter(gcnew FileStream("file.dat", FileMode::Create, FileAccess::Write, FileShare::None));

Writing to the StreamWriter, after you have created it, is no different than writing to the console. You should be very familiar with the Write() and WriteLine() methods. Reading is a little trickier, as you can read one character, an array of characters, or the rest of the characters in the stream. In most cases, you will most likely be using the StreamReader methods ReadLine() and ReadToEnd(). The first reads a single line of text, while the second reads all the text remaining on the stream. Both return their results as a String.

String ^in1 = sr->ReadLine();

String ^in2 = sr->ReadToEnd();

Listing 8-5 shows the StreamWriter and StreamReader classes in action and demonstrates many of the functionalities described previously. It also resembles the previous examples but, as you can see, the code is much simpler.

Listing 8-5. Working with a StreamWriter and a StreamReader

using namespace System; using namespace System::IO;

void main()

{

array<String^>^ data = gcnew array<String^> {

"This is ", "a test!", "This is only a test." };

StreamWriter ^sw = gcnew StreamWriter(gcnew FileStream("file.dat", FileMode::Create, FileAccess::Write, FileShare::None));

for (int i = 0; i < data->Length-1; i++)

{

sw->Write(data[i]);

}

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

299

sw->WriteLine(); sw->WriteLine(data[2]); sw->Close();

StreamReader ^sr = File::OpenText("file.dat");

String^ in = sr->ReadLine();

Console::WriteLine(in);

Console::WriteLine(sr->ReadToEnd());

sw->Close();

}

Figure 8-8 shows the results of StreamRW.exe displayed to the console. Figure 8-9 shows the resulting file output generated by the StreamRW.exe program. Notice that Figures 8-8 and 8-9 have the same results, as expected.

Figure 8-8. Console results of StreamRW.exe

Figure 8-9. File output of StreamRW.exe

Using BinaryReader and BinaryWriter

You have looked at I/O for Bytes and Strings. What if you want to store all the other data types, such as Booleans, integers, and floating points? This is where the BinaryReader and BinaryWriter come into play. These classes were designed specifically to handle all the .NET Framework’s built-in data types (including Byte and String).

To create a BinaryReader or BinaryWriter class, you need to use its constructor and pass it a Stream. This means, by the way, that BinaryReaders and BinaryWriters can take as a parameter a

FileStream, MemoryStream, NetworkStream, and so on.

FileStream ^fs = File::OpenRead(fname);

BinaryReader ^br = gcnew BinaryReader(fs);

MemoryStream ^ms = gcnew MemoryStream();

BinaryWriter ^br = gcnew BinaryWriter(ms);

300 C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

The process of writing with the BinaryWriter is very simple. After you create your BinaryWriter, you only need to use two more methods, Write() and Close(). The Write() method takes care of all the hard work by being made up of numerous overloaded versions of itself (one for each supported data type).

The BinaryReader class is a little harder to work with. This time, you need to work with many different read methods (one for each supported type). They all have the same syntax: Readxxx(), where xxx is the data type. Examples of read methods are ReadInt32(), ReadBoolean(), and ReadSingle().

A drawback of the BinaryReader is that you need to know the data type you are reading in before you actually do the read, so that you can make the correct call.

Listing 8-6 shows the BinaryWriter and BinaryReader classes in action and demonstrates many of the functionalities described previously. You might want to notice the special coding you need to do to handle DateTime classes.

Listing 8-6. Working with a BinaryWriter and a BinaryReader

using namespace System; using namespace System::IO;

using namespace System::Runtime::Serialization::Formatters::Binary; // ------ Player class ---------------------------------------------

ref class Player

{

String ^Name; Int32 Strength; Boolean IsMale;

DateTime CreateDate;

public:

Player();

Player (String ^Name, int Str, bool IsMale);

void Print();

void Save(String ^fname); void Load(String ^fname);

};

Player::Player()

{

}

Player::Player (String ^Name, int Str, bool IsMale)

{

this->Name

=

Name;

this->Strength

=

Str;

this->IsMale

=

IsMale;

this->CreateDate

= DateTime::Now;

}

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

301

void Player::Print()

{

Console::WriteLine("Name: {0} ({1})", Name, (IsMale ? "M" : "F")); Console::WriteLine("Str: {0}", Strength); Console::WriteLine("Date: {0}", CreateDate.ToString());

}

void Player::Save(String ^fname)

{

FileStream ^fs

=

File::OpenWrite(fname);

BinaryWriter ^bw

=

gcnew BinaryWriter(fs);

bw->Write(Name); bw->Write(Strength); bw->Write(IsMale);

// Due to multicultures this is a safe way of storing DateTimes bw->Write(CreateDate.Ticks);

bw->Close(); fs->Close();

}

void Player::Load(String ^fname)

{

FileStream

^fs =

File::OpenRead(fname);

BinaryReader ^br =

gcnew BinaryReader(fs);

Name

= br->ReadString();

Strength

= br->ReadInt32();

IsMale

= br->ReadBoolean();

// Due to multicultures this is a safe way of retrieving DateTimes CreateDate = DateTime( br->ReadInt64() );

br->Close(); fs->Close();

}

// ------- Main Function ---------------------------------------------

void main()

{

Player ^Joe = gcnew Player("Joe", 10, true); Joe->Save("Player.dat");

Console::WriteLine("Original Joe");

Joe->Print();

Player ^JoeClone = gcnew Player();

JoeClone->Load("Player.dat");

Console::WriteLine("\nCloned Joe"); JoeClone->Print();

}

302

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

Figure 8-10 shows the results of BinaryRW.exe displayed to the console. Figure 8-11 shows the resulting file output generated by the BinaryRW.exe program. Notice that Figure 8-11 is pretty unreadable unless you know the format in which it was stored. The fact that Figure 8-10 and Figure 8-11 represent the same data is not obvious.

Figure 8-10. Console results of BinaryRW.exe

Figure 8-11. File output of BinaryRW.exe

Serialization of Managed Objects

The BinaryReader and BinaryWriter classes are okay when it comes to storing small classes to disk and retrieving them later, as you saw in the last section. But classes can become quite complicated. What happens when your class has numerous member variables and/or linked objects? How do you figure out which data type belongs with which class? In what order were they saved? It can become quite a mess very quickly. Wouldn’t it be nice if you didn’t have to worry about the details and could just say, “Here’s the file I want the class saved to. Now, save it.” I’m sure you know where I’m going with this; this is the job of serialization.

Serialization is the process of storing the state of an object or member to a permanent medium, most probably to disk for later retrieval or to be transported over the network for some remote process to use, but there are many other uses for serialization. Deserialization is the process of restoring an object or member from disk, network, or wherever you serialized it to. Sounds tough, but the .NET Framework class library actually makes it quite simple to do.

Setting Up Classes for Serialization

The process of setting a class up for serialization is probably one of the easiest things that you can do in C++/CLI. You simply place the [Serializable] attribute in front of the managed object you want to serialize. Yep, that is it!

[Serializable]

ref class ClassName

{

//...

};

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

303

The reason this is possible is because all the class’s information is stored in its metadata. This metadata is so detailed that all the information regarding serializing and deserializing the class is available at runtime for the CLR to process the serialization or deserialization request.

Listing 8-7 shows the entire process of setting up the Player class for serialization. To make things interesting, I split PlayerAttr off into its own class. As you will see, even the serialization of a linked object like this only requires placing the [Serializable] attribute in front of it.

Listing 8-7. Making a Class Ready for Serialization

// ---------

Player Attribute class ------------------------------------

[Serializable]

ref class PlayerAttr

{

public:

property int Strength; property int Dexterity; property int Constitution; property int Intelligence; property int Wisdom; property int Charisma;

PlayerAttr(int Str, int Dex, int Con, int Int, int Wis, int Cha); void Print();

};

PlayerAttr::PlayerAttr(int Str, int Dex, int Con, int Int, int Wis, int Cha)

{

this->Strength

=

Str;

this->Dexterity

=

Dex;

this->Constitution = Con;

this->Intelligence

= Int;

this->Wisdom

=

Wis;

this->Charisma

=

Cha;

}

void PlayerAttr::Print()

{

Console::WriteLine("Str: {0}, Dex: {1}, Con {2}", Strength, Dexterity, Constitution);

Console::WriteLine("Int: {0}, Wis: {1}, Cha {2}", Intelligence, Wisdom, Charisma);

}

// --------

Player class ---------------------------------------

[Serializable] ref class Player

{

public:

property String ^Name; property String ^Race; property String ^Class; property PlayerAttr ^pattr;

304 C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

Player (String ^Name, String ^Race, String ^Class,

int Str, int Dex, int Con, int Int, int Wis, int Cha); void Print();

};

Player::Player (String ^Name, String ^Race, String ^Class, int Str, int Dex, int Con, int Int, int Wis, int Cha)

{

this->Name = Name; this->Race = Race; this->Class = Class;

this->pattr = gcnew PlayerAttr(Str, Dex, Con, Int, Wis, Cha);

}

void Player::Print()

{

Console::WriteLine("Name: {0}", Name); Console::WriteLine("Race: {0}", Race); Console::WriteLine("Class: {0}", Class); pattr->Print();

}

If you can’t tell, I play Dungeons and Dragons (D&D). These classes are a very simplified player character. Of course, you would probably want to use enums and check minimums and maximums and so forth, but I didn’t want to get too complicated.

BinaryFormatter vs. SoapFormatter

Before you actually serialize a class, you have to make a choice. In what format do you want to store the serialized data? Right now, the .NET Framework class library supplies you with two choices. You can store the serialized class data in a binary format or in an XML format or, more specifically, in a Simple Object Access Protocol (SOAP) format.

The choice is up to you. Binary is more compact, faster, and works well with the CLR. SOAP, on the other hand, is a self-describing readable text format that can be used with a system that doesn’t support the CLR. Which formatter type you should use depends on how you plan to use the serialized data.

It is also possible to create your own formatter. This book does not cover how to do this, because this book is about .NET, and the main reason that you might want to create your own formatter is if you are interfacing with a non-CLR (non-.NET) system that has its own serialization format. You should check the .NET Framework documentation for details on how to do this.

Serialization Using BinaryFormatter

As I hinted at previously, the process of serializing a class is remarkably easy. First off, all the code to handle serialization is found in the mscorlib.dll assembly. This means you don’t have to worry about loading any special assemblies. The hardest thing about serialization is that you have to remember that the BinaryFormatter is located in the namespace System::Runtime::Serialization::

Formatters::Binary. You have the option of using the fully qualified version of the formatter every time, but I prefer to add a using statement and save my fingers for typing more important code.

using namespace System::Runtime::Serialization::Formatters::Binary;

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

305

The simplest constructor for the BinaryFormatter is just the standard default, which takes no parameters.

BinaryFormatter ^bf = gcnew BinaryFormatter();

To actually serialize a class, you need to call the BinaryFormatter’s Serialize() method. This method takes a Stream and a class handle. Make sure you open the Stream for writing. You also need to truncate the Stream or create a new copy each time. And don’t forget to close the Stream when you’re done.

BinaryFormatter ^bf = gcnew BinaryFormatter();

FileStream ^plStream = File::Create("Player.dat"); bf->Serialize(plStream, Joe);

plStream->Close();

The process of deserializing is only slightly more complicated. This time, you need to use the deserialize() method. This method only takes one parameter, a handle to a Stream open for reading. Again, don’t forget to close the Stream after you’re finished with it. The tricky part of deserialization is that the deserialize() method returns a generic Object class. Therefore, you need to typecast it to the class of the original serialized class.

plStream = File::OpenRead("Player.dat");

Player ^JoeClone = (Player^)(bf->Deserialize(plStream)); plStream->Close();

Listing 8-8 shows the entire process of serializing and deserializing the Player class.

Listing 8-8. Serializing and Deserializing the Player Class

using namespace System; using namespace System::IO;

using namespace System::Runtime::Serialization::Formatters::Binary;

void main()

{

Player ^Joe =

gcnew Player("Joe", "Human", "Thief", 10, 18, 9, 13,10, 11);

Console::WriteLine("Original Joe");

Joe->Print();

FileStream ^plStream = File::Create("Player.dat");

BinaryFormatter ^bf = gcnew BinaryFormatter(); bf->Serialize(plStream, Joe); plStream->Close();

plStream = File::OpenRead("Player.dat");

Player ^JoeClone = (Player^)bf->Deserialize(plStream); plStream->Close();

Console::WriteLine("\nCloned Joe"); JoeClone->Print();

}

306

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

Figure 8-12 shows the results of BinFormSerial.exe displayed to the console. Figure 8-13 shows the resulting binary-formatted serialization output file generated.

Figure 8-12. Console results of BinFormSerial.exe

Figure 8-13. Binary-formatted file output of the serialization of the Player class

Serialization Using SoapFormatter

There is very little difference in the code required to serialize using the SoapFormatter when compared with the BinaryFormatter. One obvious difference is that you use the SoapFormatter object instead of a BinaryFormatter object. There is also one other major difference, but you have to be paying attention to notice it, at least until you finally try to compile the serializing application. The SoapFormatter is

C H A P T E R 8 I N P U T , O U T P U T , A N D S E R I A L I Z A T I O N

307

not part of the mscorlib.dll assembly. To use the SoapFormatter, you need to reference the .NET assembly system.runtime.serialization.formatters.soap.dll. You will also find the SoapFormatter class in the namespace System::Runtime::Serialization::Formatters::Soap, which also differs from the BinaryFormatter.

#using <system.runtime.serialization.formatters.soap.dll>

using namespace System::Runtime::Serialization::Formatters::Soap;

The biggest difference is one that doesn’t occur in the code. Instead, it’s the serialized file generated. BinaryFormatted serialization files are in an unreadable binary format, whereas SoapFormatted serialization files are in a readable XML text format.

Listing 8-9 shows the entire process of serializing and deserializing the Player class using the SoapFormatter. Notice that the only differences between SOAP and binary are the #using and using statements and the use of SoapFormatter instead of BinaryFormatter.

Listing 8-9. Serializing and Deserializing the Player Class Using SoapFormatter

#using <system.runtime.serialization.formatters.soap.dll>

using namespace System; using namespace System::IO;

using namespace System::Runtime::Serialization::Formatters::Soap;

int main(void)

{

Player ^Joe = gcnew Player("Joe", "Human", "Thief", 10, 18, 9, 13,10, 11);

Console::WriteLine("Original Joe");

Joe->Print();

FileStream ^plStream = File::Create("Player.xml");

SoapFormatter ^sf = gcnew SoapFormatter(); sf->Serialize(plStream, Joe); plStream->Close();

plStream = File::OpenRead("Player.xml");

Player ^JoeClone = (Player^)sf->Deserialize(plStream); plStream->Close();

Console::WriteLine("\nCloned Joe"); JoeClone->Print();

}

Figure 8-14 shows the resulting SOAP-formatted serialization output file generated by SoapFormSerial.exe.