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

C# Bible - Jeff Ferguson, Brian Patterson, Jason Beres

.pdf
Скачиваний:
64
Добавлен:
24.05.2014
Размер:
4.21 Mб
Скачать

public static implicit operator MainClass(TestClass Source)

{

return Source.MyMainClassObject;

}

}

public class MainClass

{

public static void Main()

{

object MyObject;

MainClass MyMainClassObject;

MyObject = new TestClass(); MyMainClassObject = (MainClass)MyObject;

}

}

The code in the Main() method creates a new TestClass object and then casts the object to another object of type MainClass. Because an implicit operator is defined in TestClass, this cast should succeed. However, if you execute Listing 20-1, you receive the following message on your console:

Unhandled Exception: System.InvalidCastException: Exception of type System.InvalidCastException was thrown.

at MainClass.Main()

Why does the CLR deem the conversion illegal even when an implicit operator is defined? The issue here is that the conversion works between compile-time types, not runtime types.

The Main() method creates a new object of type TestClass and assigns the new object to a variable of type object. This variable is then casted to a type of class MainClass. Because the object was created as an object of type TestClass, you might expect the cast to convert from an object of type TestClass to an object of type MainClass.

However, C# does not perform casts based on the type used when the object is created. Instead, it performs casts based on the type of variable used to hold the new object. In Listing 20-1, the new object is assigned to a variable of type object. Therefore, the C# compiler generates code that casts an object from type object to type MainClass. Because a conversion from object to MainClass is not defined, the cast conversion fails.

Understanding Structure Initialization

As you know, structures can contain language constructs also found in classes, including methods, fields, and properties. You assign values to a structure's methods and fields using a simple assignment statement. However, it's important to remember that the difference between a property and a field is that assigning a value to a property executes code compiled into the structure. This means that extra care must be taken when assigning values to properties of newly created structures. You take a look at this issue in this section.

Initializing structures

Listing 20-2 contains two structures. The first structure is called StructWithPublicMembers and contains two public integer members called X and Y. The second structure is called StructWithProperties and contains public properties called X and Y, which manage two private integers.

The Main() method in Listing 20-2 creates two variables, one having the StructWithPublicMembers structure type and another having the StructWithProperties structure type. The code then assigns values to the X and Y members of each structure.

Listing 20-2: Accessing Structures with Properties and Public Members

public struct StructWithPublicMembers

{

public int X; public int Y;

}

public struct StructWithProperties

{

private int PrivateX; private int PrivateY;

public int X

{

get

{

return PrivateX;

}

set

{

PrivateX = value;

}

}

public int Y

{

get

{

return PrivateY;

}

set

{

PrivateY = value;

}

}

}

public class MainClass

{

public static void Main()

{

StructWithPublicMembers MembersStruct; StructWithProperties PropertiesStruct;

MembersStruct.X = 100;

MembersStruct.Y = 200;

PropertiesStruct.X = 100;

PropertiesStruct.Y = 200;

}

}

Listing 20-2 does not compile. The C# compiler issues the following error:

error CS0165: Use of unassigned local variable 'PropertiesStruct'

The interesting aspect of this error is that it references the second structure variable defined in the Main() method. The C# compiler does, however, compile the code that works with the first structure variable. In other words, accessing public members in Listing 20-2 is acceptable, but accessing public properties in Listing 20-2 is not. What is the difference?

Resolving initialization problems

The short answer is that the code must use the new operator to create a new structure instance. Listing 20-2 compiles if new structure references are created:

StructWithProperties

PropertiesStruct = new

StructWithProperties();

 

The reasoning behind this answer has to do with the fact that properties are implemented by the C# compiler as public functions when it generates the Microsoft Intermediate Language (MSIL) code for the application. You can prove this by looking at the MSIL generated by the C# compiler. The .NET Framework ships with a tool called ILDASM, which stands for ILDisASseMbler. You can use this tool to examine the Intermediate Language (IL) and metadata for any CLR-compatible binary output by a .NET compiler.

Modify Listing 20-2 to include the new operation and compile it to an executable called Listing20-2.exe. After the executable is generated, you can look into the executable's contents with ILDASM. Use the following command line to start the IL disassembler with the executable from Listing 20-2:

ildasm Listing20-2.exe

Figure 20-1 shows the open ILDASM window with the Listing20-2.exe executable loaded. The executable contains the manifest, which includes identification information for the executable, and also contains metadata describing the code found in the executable.

Figure 20-1: ILDASM loaded with the executable from Listing 20-2

ILDASM displays the executable's classes and structures in a tree view format. The StructWithPublicMembers portion of the tree shows two public variables of type int32 called X and Y. These variables reflect the two properties designed into the structure.

The StructWithProperties portion of the tree shows the two private variables; it also shows four methods that weren't written into the structure:

int32 get_X()

int32 get_Y()

void set_X(int32)

void set_Y(int32)

These methods actually implement the structure's property access. The get_X() method contains the code from the get accessor of the X property, and the get_Y() method contains the code from the get accessor of the Y property. Likewise, the set_X() method contains the code from the set accessor of the X property, and the set_Y() method contains the code from the set accessor of the Y property. This means that when your code accesses a property, you are actually calling a method that implements the functionality of the property.

The problem with Listing 20-2 is that the PropertiesStruct variable is not initialized with a new operator before it is used. This means that the variable is not associated with a structure instance, and methods cannot be called on instances that don't exist. The property statements in the Main() method force the underlying property methods to be called, but there is no instance to use for the call. The C# compiler detects this problem and issues the error message shown after Listing 20-2.

The public member initialization is successful because classes and structures can be initialized directly, without using a constructor, as long as all of the instance's variables have been explicitly given a value.

Understanding Derived Classes

When discussed with regards to C# classes, you saw how classes can be derived from other classes. Derived classes inherit functionality from their parent, or base, class. The relationship between a derived class and a base class is referred to as an "is-a" relationship in objectoriented terms. All classes in C# ultimately derive from the .NET System.Object type, for example, so you can say that your class "is-a" System.Object.

Because all derived classes inherit functionality from their base class, you might assume that objects of a derived class can be used anywhere where an object of the class' base class is expected. The type safety built into the C# language takes precedence, however, and the code in this section explains the issue.

Passing derived classes

Listing 20-3 contains a class called TestClass that contains a public method called Test(). The Test() method accepts a reference to an object as a parameter.

Listing 20-3: Passing Strings Where Objects Are Expected

public class TestClass

{

public void Test(ref object ObjectReference)

{

}

}

public class MainClass

{

public static void Main()

{

TestClass TestObject = new TestClass(); string TestString = "Hello from C#!";

TestObject.Test(ref TestString);

}

}

Listing 20-3 also contains a class called MainClass, which creates both an object of the class TestClass and a string. The string is used as the parameter to the call to the TestClass object's Test() method.

Listing 20-3 does not compile. The C# compiler issues the following errors:

Listing20-3.cs(16,9): error CS1502: The best overloaded method match for 'TestClass.Test(ref object)' has some invalid arguments

Listing20-3.cs(16,29): error CS1503: Argument '1': cannot convert from 'ref string' to 'ref object'

At first glance, this doesn't seem reasonable. Because every .NET data type is an object, any data type should ultimately be converted into a variable of type object. This should include

the string object. Why can't the string object be implicitly converted into a variable of type object?

Resolving issues that arise when passing derived classes

The compiling problem stems from the strict type safety rules built into the C# language. The Test() method in Listing 20-3 takes a value of type object as a parameter, and the compiler issues an error if anything other than an object is supplied.

Another problem with the approach in Listing 20-3 results from the ref parameter modifier used in the Test() method. The ref modifier enables the method implementation to change the value of the variable, and the change is visible to the caller. This gives the Test() method the license to overwrite the ObjectReference variable with any value that can be casted back to an object. Consider the following alternative implementation of the Test() method:

public void Test(ref object ObjectReference)

{

TestClass TestObject;

TestObject = new TestClass(); ObjectReference = (object)TestObject;

}

In this implementation, an object of class TestClass is created, the ObjectReference reference variable is assigned to the new object, and the caller sees this assignment.

The problem is that Listing 20-3 passes a string into the Test() method. With this new implementation of the Test() method placed in Listing 20-3, the caller would pass a string in and would get a TestClass object back. The caller might not expect the variable's type to be changed to a different type than what the caller originally supplied, and numerous problems would arise if the caller continued to work with the code under the assumption that the variable is still a string. C# avoids this problem by mandating that only correct parameter types be used when methods are called.

Using Non-Integers as Array Elements

The C# language specifies that only integers can be used as array elements. Sometimes, however, you run into situations in which integers are not the most convenient way to express an array index in your code. Consider a chessboard, for example, where a letter from A through H traditionally references the eight columns of the board, and a number from 1 through 8 traditionally references the eight rows of the board. If you need to represent a chessboard in your C# code, you might choose to use a two-dimensional rectangular array:

int [,] Chessboard;

Chessboard = new int [8,8];

After the chessboard array is initialized, you might want to reference a cell using the traditional chessboard cell syntax. You might want to reference cell B7, for example, as follows:

Chessboard['B',7];

You can't do this, however, because C# does not let you use characters, such as the B in this example, as array element references.

Listing 20-4 uses an indexer to solve this problem:

Listing 20-4: Representing a Chessboard with an Indexer

using System;

public class Chessboard

{

private int [,] Board;

public Chessboard()

{

Board = new int[8,8];

}

public int this [char Column, int RowIndex]

{

get

{

int ColumnIndex;

switch(Column)

{

case 'A': case 'a':

ColumnIndex = 0; break;

case 'B': case 'b':

ColumnIndex = 1; break;

case 'C': case 'c':

ColumnIndex = 2; break;

case 'D': case 'd':

ColumnIndex = 3; break;

case 'E': case 'e':

ColumnIndex = 4; break;

case 'F': case 'f':

ColumnIndex = 5; break;

case 'G': case 'g':

ColumnIndex = 6; break;

case 'H': case 'h':

ColumnIndex = 7; break;

default:

throw new Exception("Illegal column

specifier.");

}

Console.WriteLine("(returning cell [{0},{1}]", ColumnIndex, RowIndex);

return Board[ColumnIndex, RowIndex];

}

}

}

public class MainClass

{

public static void Main()

{

int CellContents;

Chessboard MyChessboard = new Chessboard();

CellContents = MyChessboard ['B', 7];

}

}

The code in Listing 20-4 declares a class called Chessboard to represent a chessboard. The class includes a private, two-dimensional array of integers called Board, which can be used to represent which chess pieces are on which cells in the board. (In the interests of keeping the code listing simple, the array is not actually used.)

The class also implements an indexer with a get accessor. The indexer accepts two parameters: a character and an integer. The indexer assumes that the character specified in the first parameter represents a column identifier, and it translates the character into a column in the private array. A column specifier of A translates to column 0 in the private Board array; a column specifier of B translates to column 1 in the private Board array, and so on. The indexer writes a message specifying the translated index elements to the console and then returns the value of the array element referenced by the indexer parameters.

The Main() method in Listing 20-4 creates a new object of type Chessboard and uses the indexer to reference square B7:

CellContents = MyChessboard ['B', 7];

When the code in Listing 20-4 is run, the code in the Main() method calls the indexer, which prints the following out to the console:

(returning cell [1,7]

The indexer translated the first parameter, B, into the integer-based column reference that the C# compiler needs to access an element in the private array. This scheme enables you to design classes that use a syntax that is natural to the application - character-based array element indexes, in this case - while still adhering to the C# requirement of integer-based array element indexes.

Summary

The best way to solve a tough C# programming problem is to try different coding methods with the compiler. Don't be afraid to experiment. The C# compiler tells you if you do something wrong.

You might also consider making use of the ILDASM tool to get under the hood of your assemblies and see how the C# compiler makes executable code from your source code. Understanding the code that the C# compiler generates can help your understanding of why your code works the way it does. In this chapter, you learned that the property accessor code that you write is turned into special methods by the C# compiler. This may be hard to figure out without looking at the code generated by ILDASM. The ILDASM tool works with any assembly produced by a .NET compiler that generates MSIL. Examine some assemblies and

.NET applications with ILDASM. Load them into the tool and see how they are built. Looking at other assemblies and their content can give you a better understanding of how

.NET code works, and can help make you a better .NET developer.

Part IV: Developing .NET Solutions Using

C#

Chapter List

Chapter 21: Building WindowsForms Application

Chapter 22: Creating Web Applications with WebForms

Chapter 23: Database Programming with ADO.NET and XML

Chapter 24: Working with Files and the Windows Registry

Chapter 25: Accessing Data Streams

Chapter 26: Drawing with GDI+

Chapter 27: Building Web Services

Chapter 28: Using C# in ASP.NET

Chapter 29: Building Custom Controls

Chapter 30: Building Mobile Applications

Chapter 21: Building WindowsForms

Applications

In This Chapter

Much of the material written about the .NET Framework has centered on the support given to developers writing Internet applications. The ASP.NET engine and the "software as a service" development models are indeed powerful tools for developing Internet applications. However, the .NET Framework is not just about the Internet.

Microsoft realizes that although many developers are writing Internet-based applications, many other developers are writing desktop-based, Win32-style desktop applications. The

.NET Framework has not forgotten these developers. It includes a set of .NET classes that

makes it easy to develop Windows desktop applications using a .NET-compatible language. This set of classes and the programming model that it supports is called WindowsForms.

In this chapter, you take a look at the basic structure of a WindowsForms application. You will see how a basic form is created and how controls are added to forms. We'll take a look at the .NET Framework classes available for use in a WindowsForms application, and we'll also take a look at some of the assembly attributes that you can use to add version and copyright information to your applications.

Understanding WindowsForms Architecture

Developing a WindowsForms application requires that you understand how the WindowsForms base classes are used with your .NET applications. This section examines the architecture of the WindowsForms class library.

All the classes that you use to build your WindowsForms applications reside in a .NET Framework namespace called System.Windows.Forms. This namespace contains all the classes that you need to build rich Windows desktop applications. These classes enable you to work with forms, buttons, edit controls, check boxes, lists, and many other user-interface elements. All of these classes are at your disposal and ready to use in your WindowsForms applications.

WindowsForms applications make use of two fundamental .NET Framework classes: the Form class, which manages the application's forms and the controls placed on the form, and the Application class, which manages the application's handling of Windows messages sent to and received from the forms in the application. Both of these classes reside in the .NET Framework's System.Windows.Forms namespace and make up the basic anatomy of a WindowsForms application.

The Form class

The System.Windows.Forms namespace includes a base class called Form. The Form class represents a form, or window, in your application. When you create a WindowsForms application in C#, you design a window class and use the Form class as the base class for your window class. Your window class inherits all the basic behavior of a window and adds the functionality you need to fit the needs of your application. All the basic window behaviors are built into the base Form class, and you inherit all of that behavior automatically if you derive your window classes from the Form class.

The Application class

Form classes define the look and feel of a window in your application, but they do not run by themselves. WindowsForms must be run within the context of an application. The System.Windows.Forms namespace includes a class called Application, which contains methods that help you manage your WindowsForms application.

The Application class contains methods that enable you to start, manage, and stop WindowsForms applications. WindowsForms respond to events initiated by the user, such as moving the mouse or pressing a key on the keyboard. Since the very early days of Windows,

Соседние файлы в предмете Программирование