【打不死的猫注:
本文中涉及的测试程序可从如下地址下载:
http://download.microsoft.com/download/2/8/c/28c4ace3-f5ed-4e14-bc64-3d563b807dfb/CtoCsharp.exe
本文转载自MSDN,原文由鼎鼎大名的Jesse Liberty撰写,原文地址是:
http://msdn.microsoft.com/msdnmag/issues/01/07/ctocsharp/default.aspx】
What You Need to Know to Move from C++ to C#
Jesse Liberty
This article assumes you're familiar with C++
SUMMARY
C# builds on the syntax and semantics of C++, allowing C programmers to take advantage of .NET and the common language runtime. While the transition from C++ to C# should be a smooth one, there are a few things to watch out for including changes to new, structs, constructors, and destructors. This article explores the language features that are new to C# such as garbage collection, properties, foreach loops, and interfaces. Following a discussion of interfaces, there's a discussion of properties, arrays, and the base class libraries. The article concludes with an exploration of asynchronous I/O, attributes and reflection, type discovery, and dynamic invocation.
very 10 years or so, developers must devote time and energy to learning a new set of programming skills. In the early 1980s it was Unix and C; in the early 1990s it was Windows® and C++; and today it is the Microsoft® .NET Framework and C#. While this process takes work, the benefits far outweigh the costs. The good news is that with C# and .NET the analysis and design phases of most projects are virtually unchanged from what they were with C++ and Windows. That said, there are significant differences in how you will approach programming in the new environment. In this article I'll provide information about how to make the leap from programming in C++ to programming in C#.
Many articles (for example, Sharp New Language: C# Offers the Power of C++ and Simplicity of Visual Basic) have explained the overall improvements that C# implements, and I won't repeat that information here. Instead, I'll focus on what I see as the most significant change when moving from C++ to C#: going from an unmanaged to a managed environment. I'll also warn you about a few significant traps awaiting the unwary C++ programmer and I'll show some of the new features of the language that will affect how you implement your programs.
Moving to a Managed Environment
C++ was designed to be a low-level platform-neutral object-oriented programming language. C# was designed to be a somewhat higher-level component-oriented language. The move to a managed environment represents a sea change in the way you think about programming. C# is about letting go of precise control, and letting the framework help you focus on the big picture.
For example, in C++ you have tremendous control over the creation and even the layout of your objects. You can create an object on the stack, on the heap, or even in a particular place in memory using the placement operator new.
With the managed environment of .NET, you give up that level of control. When you choose the type of your object, the choice of where the object will be created is implicit. Simple types (ints, doubles, and longs) are always created on the stack (unless they are contained within other objects), and classes are always created on the heap. You cannot control where on the heap an object is created, you can't get its address, and you can't pin it down in a particular memory location. (There are ways around these restrictions, but they take you out of the mainstream.)
You no longer truly control the lifetime of your object. C# has no destructor. The garbage collector will take your item's storage back sometime after there are no longer any references to it, but finalization is nondeterministic.
The very structure of C# reflects the underlying framework. There is no multiple inheritance and there are no templates because multiple inheritance is terribly difficult to implement efficiently in a managed, garbage-collected environment, and because generics have not been implemented in the framework.
The C# simple types are nothing more than a mapping to the underlying common language runtime (CLR) types. For example, a C# int maps to a System.Int32. The types in C# are determined not by the language, but by the common type system. In fact, if you want to preserve the ability to derive C# objects from Visual Basic® objects, you must restrict yourself further, to the common language subset—those features shared by all .NET languages.
On the other hand, the managed environment and CLR bring a number of tangible benefits. In addition to garbage collection and a uniform type system across all .NET languages, you get a greatly enhanced component-based language, which fully supports versioning and provides extensible metadata, available at runtime through reflection. There is no need for special support for late binding; type discovery and late binding are built into the language. In C#, enums and properties are first-class members of the language, fully supported by the underlying engine, as are events and delegates (type-safe function pointers).
The key benefit of the managed environment, however, is the .NET Framework. While the framework is available to any .NET language, C# is a language that's well-designed for programming with the framework's rich set of classes, interfaces, and objects.
Traps
C# looks a lot like C++, and while this makes the transition easy, there are some traps along the way. If you write what looks like perfectly legitimate code in C++, it won't compile, or worse, it won't behave as expected. Most of the syntactic changes from C++ to C# are trivial (no semicolon after a class declaration, Main is now capitalized). I'm building a Web page which lists these for easy reference, but most of these are easily caught by the compiler and I won't devote space to them here. I do want to point out a few significant changes that will cause problems, however.
Reference and Value Types
C# distinguishes between value types and reference types. Simple types (int, long, double, and so on) and structs are value types, while all classes are reference types, as are Objects. Value types hold their value on the stack, like variables in C++, unless they are embedded within a reference type. Reference type variables sit on the stack, but they hold the address of an object on the heap, much like pointers in C++. Value types are passed to methods by value (a copy is made), while reference types are effectively passed by reference.
Structs
Structs are significantly different in C#. In C++ a struct is exactly like a class, except that the default inheritance and default access are public rather than private. In C# structs are very different from classes. Structs in C# are designed to encapsulate lightweight objects. They are value types (not reference types), so they're passed by value. In addition, they have limitations that do not apply to classes. For example, they are sealed, which means they cannot be derived from or have any base class other than System.ValueType, which is derived from Object. Structs cannot declare a default (parameterless) constructor.
On the other hand, structs are more efficient than classes so they're perfect for the creation of lightweight objects. If you don't mind that the struct is sealed and you don't mind value semantics, using a struct may be preferable to using a class, especially for very small objects.
Everything Derives from Object
In C# everything ultimately derives from Object. This includes classes you create, as well as value types such as int or structs. The Object class offers useful methods, such as ToString. An example of when you use ToString is with the System.Console.WriteLine method, which is the C# equivalent of cout. The method is overloaded to take a string and an array of objects.
To use WriteLine you provide substitution parameters, not unlike the old-fashioned printf. Assume for a moment that myEmployee is an instance of a user-defined Employee class and myCounter is an instance of a user-defined Counter class. If you write the following code
What happens if you pass integer values to WriteLine? You can't call ToString on an integer, but the compiler will implicitly box the int in an instance of Object whose value will be set to the value of the integer. When WriteLine calls ToString, the object will return the string representation of the integer's value (see Figure 1).The employee: Employee, the counter value: 12
Note that you need to use the ref keyword in both the method declaration and the actual call to the method.public void GetStats( ref int age, ref int ID, ref int yearsServed)
You can now declare age, ID, and yearsServed in the calling method and pass them into GetStats and get back the changed values.Fred.GetStats( ref age, ref ID, ref yearsServed);
Again, the calling method must match.public void GetStats( out int age, out int ID, out int yearsServed)
Fred.GetStats( out age, out ID, out yearsServed);
the compiler will pass in the value 17 as value.Fred.Age = 17 ;
If you change your driver program to use these accessors, you can see how they work (see Figure 3).public int YearsServed
{
get
{
return yearsServed;
}
}
Otherwise, you can initialize it like this:int [] myIntArray = new int [ 5 ];
You can create a 4×3 rectangular array like this:int [] myIntArray = { 2 , 4 , 6 , 8 , 10 };
Alternatively, you can simply initialize it, like this:int [,] myRectangularArray = new int [rows, columns];
Since jagged arrays are arrays of arrays, you supply only one dimensionint [,] myRectangularArray =
{
{0,1,2}, {3,4,5}, {6,7,8}, {9,10,11}
} ;
and then create each of the internal arrays, like so:int [][] myJaggedArray = new int [ 4 ][];
Because arrays derive from the System.Array object, they come with a number of useful methods, including Sort and Reverse.myJaggedArray[ 0 ] = new int [ 5 ];
myJaggedArray[ 1 ] = new int [ 2 ];
myJaggedArray[ 2 ] = new int [ 3 ];
myJaggedArray[ 3 ] = new int [ 5 ];
This is accomplished with Indexers. An Indexer is much like a property, but supports the syntax of the index operator. Figure 4 shows a property whose name is followed by the index operator.string theFirstString = myListBox[ 0 ];
string theLastString = myListBox[Length - 1 ];
Notice that the method passes the current ListBoxTest object (this) to the enumerator. That will allow the enumerator to enumerate this particular ListBoxTest object.public IEnumerator GetEnumerator()
{
return (IEnumerator) new ListBoxEnumerator(this);
}
The MoveNext method increments the index and then checks to ensure that you have not run past the end of the object you're enumerating. If you have, you return false; otherwise, true is returned.public ListBoxEnumerator(ListBoxTest theLB)
{
myLBT = theLB;
index = -1;
}
Reset does nothing but reset the index to -1.public bool MoveNext()
{
index++;
if (index >= myLBT.myStrings.Length)
return false;
else
return true;
}
That's all there is to it. The call to foreach fetches the enumerator and uses it to enumerate over the array. Since foreach will display every string whether or not you've added a meaningful value, I've changed the initialization of myStrings to eight items to keep the display manageable.public object Current
{
get
{
return(myLBT[index]);
}
}
myStrings = new String[ 8 ];
When you include System, you do not automatically include all its subsidiary namespaces, each must be explicitly included with the using keyword. Since you'll be using the I/O stream classes, you'll need System.IO, and you want System.Text to support ASCII encoding of your byte stream, as you'll see shortly.using System;
using System.IO;
using System.Text;
The member variable inputStream is of type Stream, and it is on this object that you will call the BeginRead method, passing in the buffer as well as the delegate (myCallBack). A delegate is very much like a type-safe pointer to member function. In C#, delegates are first-class elements of the language.public class AsynchIOTester
{
private Stream inputStream;
private byte[] buffer;
private AsyncCallback myCallBack;
Thus, this delegate may be associated with any method that returns void and takes an IAsyncResult interface as a parameter. The CLR will pass in the IAsyncResult interface object at runtime when the method is called; you only have to declare the methodpublic delegate void AsyncCallback (IAsyncResult ar);
The call to new fires up the constructor. In the constructor you open a file and get a Stream object back. You then allocate space in the buffer and hook up the callback mechanism.public static void Main()
{
AsynchIOTester theApp = new AsynchIOTester();
theApp.Run();
}
In the Run method, you call BeginRead, which will cause an asynchronous read of the file.AsynchIOTester()
{
inputStream = File.OpenRead(@"C:\MSDN\fromCppToCS.txt");
buffer = new byte[BUFFER_SIZE];
myCallBack = new AsyncCallback(this.OnCompletedRead);
}
You then go on to do other work.inputStream.BeginRead(
buffer, // where to put the results
0 , // offset
buffer.Length, // how many bytes (BUFFER_SIZE)
myCallBack, // call back delegate
null ); // local state object
When the read completes, the CLR will call the callback method.for ( long i = 0 ; i < 50000 ; i ++ )
{
if (i%1000 == 0)
{
Console.WriteLine("i: {0}", i);
}
}
The first thing you do in OnCompletedRead is find out how many bytes were read by calling the EndRead method of the Stream object, passing in the IAsyncResult interface object passed in by the common language runtime.void OnCompletedRead(IAsyncResult asyncResult)
{
The result of this call to EndRead is to get back the number of bytes read. If the number is greater than zero, convert the buffer into a string and write it to the console, then call BeginRead again for another asynchronous read.int bytesRead = inputStream.EndRead(asyncResult);
Now you can do other work (in this case, counting to 50,000) while the reads are taking place, but you can handle the read data (in this case, by outputting it to the console) each time a buffer is full. The complete source code for this example, AsynchIO.cs, is available for download from the link at the top of this article.if (bytesRead > 0 )
{
String s = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine(s);
inputStream.BeginRead(buffer, 0, buffer.Length,
myCallBack, null);
}
Once constructed, ask the TCPListener object to start listening.TCPListener tcpListener = new TCPListener( 65000 );
Now wait for a client to request a connection.tcpListener.Start();
The Accept method of the TCPListener object returns a Socket object, which represents a standard Berkeley socket interface and which is bound to a specific end point (in this case, the client). Accept is a synchronous method and will not return until it receives a connection request. If the socket is connected, you're ready to send the file to the client.Socket socketForClient = tcpListener.Accept();
if (socketForClient.Connected)
{
•••
Next you have to create a NetworkStream class, passing the socket in to the constructor.
Then create a StreamWriter object much as you did before, except this time not on a file, but on the NetworkStream you just created.NetworkStream networkStream = new NetworkStream(socketForClient);
When you write to this stream, the stream is sent over the network to the client. The complete source code, TCPServer.cs, is also available for download.System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(networkStream);
With this TCPClient, you can create a NetworkStream, and on that stream create a StreamReader.TCPClient socketForServer;
socketForServer = new TCPClient( " localHost " , 65000 );
Now, read the stream as long as there is data on it, and output the results to the console.NetworkStream networkStream = socketForServer.GetStream();
System.IO.StreamReader streamReader = new System.IO.StreamReader(networkStream);
To test this, you create a simple test file:do
{
outputString = streamReader.ReadLine();
if( outputString != null )
{
Console.WriteLine(outputString);
}
}
while ( outputString != null );
Here is the output from the server:This is line one
This is line two
This is line three
This is line four
And here is the output from the client:Output (Server)
Client connected
Sending This is line one
Sending This is line two
Sending This is line three
Sending This is line four
Disconnecting from client
Exiting
This is line one
This is line two
This is line three
This is line four
[assembly: AssemblyDelaySign(false)] [assembly: AssemblyKeyFile(".\\keyFile.snk")]or by separating the attributes with commas.
[assembly: AssemblyDelaySign(false), assembly: AssemblyKeyFile(".\\keyFile.snk")]
Custom Attributes
You are free to create your own custom attributes and to use them at runtime as you see fit. For example, you might create a documentation attribute to tag sections of code with the URL of associated documentation. Or you might tag your code with code review comments or bug fix comments.
Suppose your development organization wants to keep track of bug fixes. It turns out you keep a database of all your bugs, but you'd like to tie your bug reports to specific fixes in the code. You might add comments to your code similar to the following:
// Bug 323 fixed by Jesse Liberty 1/1/2005.This would make it easy to see in your source code, but it would be nice if you could extract this information into a report or keep it in a database so that you could search for it. It would also be nice if all the bug report notations used the same syntax. A custom attribute may be just what you need. You would then replace your comment with something like this:
[BugFix(323,"Jesse Liberty","1/1/2005") Comment="Off by one error"]Attributes, like most things in C#, are classes. To create a custom attribute, you derive your new custom attribute class from System.Attribute.
public class BugFixAttribute : System.AttributeYou need to tell the compiler what kinds of elements this attribute can be used with (the attribute target). You specify this with (what else?) an attribute.
[AttributeUsage(AttributeTargets.ClassMembers, AllowMultiple = true)]AttributeUsage is an attribute applied to attributes—a meta attribute. It provides, if you will, meta-metadata; that is data about the metadata. In this case, you pass two arguments: the first is the target (in this case class members) and a flag indicating whether a given element may receive more than one such attribute. AllowMultiple has been set to true, which means a class member may have more than one BugFixAttribute assigned.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]This would allow the attribute to be attached to either a Class or an Interface.
[BugFix(123, "Jesse Liberty", "01/01/05", Comment="Off by one")]The compiler will first look for an attribute named BugFix and, not finding that, will then look for BugFixAttribute.
public BugFixAttribute(int bugID, string programmer, string date) { this.bugID = bugID; this.programmer = programmer; this.date = date; }Named parameters are implemented as properties.
[BugFixAttribute(121,"Jesse Liberty","01/03/05")] [BugFixAttribute(107,"Jesse Liberty","01/04/05", Comment="Fixed off by one errors")] public class MyMathThese attributes will be stored with the metadata. Figure 6 provides the complete source code. The following shows the output:
Calling DoFunc(7). Result: 9.3333333333333339As you can see, the attributes had absolutely no impact on the output, and creating attributes has no impact on performance. In fact, for the moment, you have only my word that the attributes exist at all. A quick look at the metadata using ILDASM does reveal that the attributes are in place, however, as shown in Figure 7.
System.Reflection.MemberInfo inf = typeof(MyMath);Call the typeof operator on the MyMath type, which returns an object of type Type, which derives from MemberInfo.
object[] attributes; attributes = Attribute.GetCustomAttributes(inf, typeof(BugFixAttribute));You can now iterate through this array, printing out the properties of the BugFixAttribute object, as shown in Figure 8. When this replacement code is put into the listing in Figure 6, the metadata is displayed.
public static Assembly.Load(AssemblyName)Then you should pass in the core library.
Assembly a = Assembly.Load("Mscorlib.dll");Once the assembly is loaded, you can call GetTypes to return an array of Type objects. The Type object is the heart of reflection. Type represents type declarations: classes, interfaces, arrays, values, and enumerations.
Type[] types = a.GetTypes();The assembly returns an array of types that can be displayed in a foreach loop. The output from this will fill many pages. Here is a short excerpt from the output:
Type is System.TypeCode Type is System.Security.Util.StringExpressionSet Type is System.Text.UTF7Encoding$Encoder Type is System.ArgIterator Type is System.Runtime.Remoting.JITLookupTable 1205 types foundYou have obtained an array filled with the types from the core library, and printed them one by one. As the output shows, the array contains 1,205 entries.
public class Tester { public static void Main() { // examine a single object Type theType = Type.GetType("System.Reflection.Assembly"); Console.WriteLine("\nSingle Type is {0}\n", theType); } }The output looks like this:
Single Type is System.Reflection.Assembly
System.String s_localFilePrefix is a Field Boolean IsDefined(System.Type) is a Method Void .ctor() is a Constructor System.String CodeBase is a Property System.String CopiedCodeBase is a Property
MemberInfo[] mbrInfoArray = theType.GetMembers(BindingFlags.LookupAll);Then you add a call to GetMethods.
mbrInfoArray = theType.GetMethods();The output now is nothing but the methods.
Output (excerpt) Boolean Equals(System.Object) is a Method System.String ToString() is a Method System.String CreateQualifiedName(System.String, System.String) is a Method System.Reflection.MethodInfo get_EntryPoint() is a Method
System.Type[] GetTypes() is a Method System.Type[] GetExportedTypes() is a Method System.Type GetType(System.String, Boolean) is a Method System.Type GetType(System.String) is a Method System.Reflection.AssemblyName GetName(Boolean) is a Method System.Reflection.AssemblyName GetName() is a Method Int32 GetHashCode() is a Method System.Reflection.Assembly GetAssembly(System.Type) is a Method System.Type GetType(System.String, Boolean, Boolean) is a Method
Type theMathType = Type.GetType("System.Math");With that type information, you can dynamically load an instance of that class.
Object theObj = Activator.CreateInstance(theMathType);CreateInstance is a static method of the Activator class, which can be used to instantiate objects.
Type[] paramTypes = new Type[1]; paramTypes[0]= Type.GetType("System.Double");You can now pass the name of the method you want and this array describing the types of the parameters to the GetMethod method of the type object retrieved earlier.
MethodInfo CosineInfo = theMathType.GetMethod("Cos",paramTypes);You now have an object of type MethodInfo on which you can invoke the method. To do so, you must pass in the actual value of the parameters, again in an array.
Object[] parameters = new Object[1]; parameters[0] = 45; Object returnVal = CosineInfo.Invoke(theObj,parameters);Note that I've created two arrays. The first, paramTypes, held the type of the parameters; the second, parameters, held the actual value. If the method had taken two arguments, you would have declared these arrays to hold two values. If the method took no values, you still would create the array, but you would give it a size of zero!
Type[] paramTypes = new Type[0];Odd as this looks, it is correct. Figure 11 shows the complete code.