Solving Real-World Problems
|Purity for purity's sake is of little use to the professional programmer.|
One of the drawbacks of C++ is its complexity; simplicity was an important design goal for C#.
It's possible to go overboard on simplicity and language purity but purity for purity's sake is of little use to the professional programmer. We therefore tried to balance our desire to have a simple and concise language with solving the real-world problems that programmers face. We've also kept in mind the difference between class author complexity and class consumer complexity; if adding complexity in the author's world simplifies the consumer's world significantly it's worth considering. (See Sidebar: Inside the C# Design Team
Value types, operator overloading and user-defined conversions all add complexity to the language, but allow an important user scenario to be tremendously simplified.
We also elected to allow the user to perform some pointer-based operations from within C#. Because such operations can't be verified by the runtime to be type-safe, they are known as unsafe operations and the runtime only allows them to be executed if the code is fully trusted. Full trust is only granted to code that is local to the machine, so if such code is part of a Web page or on a network share, it won't run. The ability to use unsafe code isn't a commonly-used feature but, in some cases, it's critical to get the performance you need or to interoperate with existing code.
The .NET Runtime also provides solutions to important real-world problems. One of the perennial problems with software running on Windows is "DLL Hell," which occurs when one version of software installs a different version of a DLL that another program depends on. The problem is solved in the .NET world by adding version information to an assembly. This allows different versions of the same assembly to co-exist "side-by-side" on the same system and programs only use the version they were built and tested against.
In addition, multiple versions of the runtime can exist on the same machine with each program using the version is was built and tested for. These side-by-side features make code much more robust.
Another important problem addressed by .NET Developer Platform is the use of existing code. The last thing most programmers want to do is to take existing, well-debugged code and port it to another language. The runtime provides interop features that enable .NET code to use existing COM components or code contained in DLLs, and the Managed Extensions to C++ enable the user to mix new managed code with existing unmanaged C++ code.
While we didn't want to make wholesale changes to the C++ syntax, we did do a bit of tweaking to make the programmer's job easier. One of the areas we addressed was how programmers deal with arrays and collections. One of the most common tasks is traversing the elements of an array. Here's a bit of code I've written thousands of times:
for (int i = 0; i < arr.Count; i++)
string s = (string) arr[i];
// use s here
As I write that loop, there are a number of decisions that I make. First, I need to choose the name for a loop index. When I choose that name, I have to do a mental check to make sure I'm choosing a name I have not used before. Next, I have to set the termination condition. I have to remember what I'm iterating over and remember how to figure out how long it is. In the body of the loop, I have to remember the array and index names again and the type of the array elements.
The only two important pieces of information are the name of the array and the type of the array elements. The rest is just busy work that the programmer has to spend time on. C# adopts the foreach construct found in languages such as Perl, which simplifies the code to:
foreach (string s in arr)
// use s here
The foreach construct doesn't require anything extra; each piece of information is only mentioned once. This not only makes it much harder to make a mistake but it also makes it clear what the code does. Foreach also enables iterating over types such as database cursors, which have no count or method of indexing.
C# also makes it easier to use primitive types in collections. To be able to use a value type such as int in a collection, you need some way to convert it to a reference type. In Java, this is done by using a wrapper class, so that storing an int requires putting it inside an Integer class instance, and that instance is then added to the collection. In this example, four integers are put into a Vector, and then printed out:
Vector vec = new Vector();
for (int i = 0; i < vec.size(); i++)
In C#, the same operation is performed automatically through boxing. Whenever a value type is used in a situation where the type object is required, a reference-type box is automatically generated, which simplifies the user code. In this example, there's no need to manually wrap the integer values or extract them out.
ArrayList arr = new ArrayList();
for (int i = 0; i < arr.Count; i++)
C# also addresses some C++ constructs that have led to common errors. A common error in C++ code is to write:
if (count = 5)
Where "=" is used instead of "==". Under the C# rules, the expression in an if statement must evaluate to true or false, so the preceding code generates an error.