• No results found

Inside C#

N/A
N/A
Protected

Academic year: 2021

Share "Inside C#"

Copied!
364
0
0

Loading.... (view fulltext now)

Full text

(1)

4

(2)

PUBLISHED BY Microsoft Press

A Division of Microsoft Corporation One Microsoft Way

Redmond, Washington 98052-6399 Copyright (c) 2001 by Tom Archer

All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by any means without the written permission of the publisher.

Library of Congress Cataloging-in-Publication Data Archer, Tom.

Inside C# / Tom Archer. p. cm.

Includes index. ISBN 0-7356-1288-9

1. C# (Computer program language) I. Title. QA76.73.C154 A73 2001

005.13'3--dc21 2001030562 Printed and bound in the United States of America. 1 2 3 4 5 6 7 8 9 QWT 6 5 4 3 2 1

Distributed in Canada by Penguin Books Canada Limited.

A CIP catalogue record for this book is available from the British Library.

Microsoft Press books are available through booksellers and distributors worldwide. For further information about international editions, contact your local Microsoft Corporation office or contact Microsoft Press International directly at fax (425) 936-7329. Visit our Web site at mspress.microsoft.com. Send comments to [email protected]. Microsoft, Microsoft Press, MSDN, .NET logo, Visual Basic, Visual C++, Visual Studio, and Windows are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. Other product and company names mentioned herein may be the trademarks of their respective owners.

The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious. No association with any real company, organization, product, domain name, e-mail address, logo, person, place, or event is intended or should be inferred.

Acquisitions Editor: Danielle Bird Project Editor: Devon Musgrave Technical Editor: Brian Johnson

(3)

3

4

1

Fundamentals of Object-Oriented Programming

The goals of this chapter are to guide you through the terminology of object-oriented programming (OOP) and to give you an understanding of the importance of object-oriented concepts to programming. Many languages, such as C++ and Microsoft Visual Basic, are said to "support objects," but few languages actually support all the principles that constitute object-oriented programming. C# is one of these languages: it was designed from the ground up to be a truly object-oriented, component-based language. So, to get the absolute maximum out of this book, you need to have a strong grasp of the concepts presented here.

I know that conceptual chapters like this are often skipped over by readers who want to dive into the code right away, but unless you consider yourself an "object guru," I encourage you to read this chapter. For those of you only

somewhat familiar with object-oriented programming, you should reap benefits from doing so. Also, keep in mind that the chapters that follow this one will refer back to the terminology and concepts discussed here.

As I've said, many languages claim to be object-oriented or object-based, but few truly are. C++ isn't, because of the undeniable, uncircumventable fact that its roots are deeply embedded in the C language. Too many OOP ideals had to be sacrificed in C++ to support legacy C code. Even the Java language, as good as it is, has some limitations as an object-oriented language. Specifically, I'm referring to the fact that in Java you have primitive types and object types that are treated and behave very differently. However, the focus of this chapter is not on comparing the faithfulness of different languages to OOP principles. Rather, this chapter will present an objective and language-agnostic tutorial on OOP principles themselves.

Before we get started, I'd like to add that object-oriented programming is much more than a marketing phrase (although it has become that for some people), a new syntax, or a new application programming interface (API). Object-oriented programming is an entire set of concepts and ideas. It's a way of thinking about the problem being addressed by a computer program and tackling the problem in a more intuitive and therefore more productive manner. My first job involved using the Pascal language to program the box-office reporting and itinerary applications for

Holiday on Ice. As I moved on to other jobs and other applications, I programmed in PL/I and RPG III (and RPG/400). After a few more years, I started programming applications in the C language. In each of these instances, I was easily able to apply knowledge I had learned from prior experience. The learning curve for each successive language was shorter regardless of the complexity of the language I was learning. This is because until I started programming in C++, all the languages I had used were procedural languages that mainly differed only in syntax.

However, if you are new to object-oriented programming, be forewarned: prior experience with other non-object-oriented languages will not help you here! Object-non-object-oriented programming is a different way of thinking about how to design and program solutions to problems. In fact, studies have shown that people who are new to programming learn object-oriented languages much more quickly than those of us who started out in procedural languages such as BASIC, COBOL, and C. These individuals do not have to "unlearn" any procedural habits that can hamper their understanding of OOP. They are starting with a clean slate. If you've been programming in procedural languages for many years and C# is your first object-oriented language, the best advice I can give you is to keep an open mind and implement the ideas I'm presenting here before you throw up your hands and say, "I can fake this in [insert your procedural language of choice]." Anyone who's come from a procedural background to object-oriented programming has gone through this learning curve, and it's well worth it. The benefits of programming with an object-oriented language are incalculable, both in terms of writing code more efficiently and having a system that can be easily modified and extended once written. It just might not seem that way at first. However, almost 20 years of developing software (including the past 8 with object-oriented languages) have shown me that OOP concepts, when applied correctly, do in fact live up to their promise. Without further ado, let's roll up our sleeves and see what all the fuss is about.

(4)

Everything Is an Object

In a true object-oriented language, all problem domain entities are expressed through the concept of objects. (Note that in this book I'll be using the Coad/Yourdon definition for "problem domain"—that is, that a problem domain is the problem you're attempting to solve, in terms of its specific complexities, terminology, challenges, and so on.) As you might guess, objects are the central idea behind object-oriented programming. Most of us don't walk around thinking in terms of structures, data packets, function calls, and pointers; instead, we typically think in terms of objects. Let's look at an example.

If you were writing an invoicing application and you needed to tally the detail lines of the invoice, which of the following mental approaches would be more intuitive from the client's perspective?

Non-object-oriented approach I'll have access to a data structure representing an invoice header. This

invoice header structure will also include a doubly linked list of invoice detail structures, each of which contains a total line amount.Therefore, to get an invoice total, I need to declare a variable named something like

totalInvoiceAmount and initialize it to 0, get a pointer to the invoice header structure, get the head of the linked list of detail lines, and then traverse the linked list of detail lines. As I read each detail line structure, I'll get its member variable containing the total for that line and increment my totalInvoiceAmount variable.

Object-oriented approach I'll have an invoice object, and I'll send a message to that object to ask it for the

total amount. I don't need to think about how the information is stored internally in the object, as I had to do with the non-object-oriented data structure. I simply treat the object in a natural manner, making requests to it by sending messages. (The group of messages that an object can process are collectively called the object's interface. In the following paragraph, I'll explain why thinking in terms of interface rather than

implementation, as I have done here, is justifiable in the object-oriented approach.)

Obviously, the object-oriented approach is more intuitive and closer to how many of us would think our way through a problem. In the second solution, the invoice object probably iterates through a collection of invoice detail objects, sending a message to each one requesting its line amount. However, if what you're looking for is the total, you don't care how it's done. You don't care because oneof the main tenets of object-oriented programming is

encapsulation—the ability of an object to hide its internal data and methods and to present an interface that makes the important parts of the object programmatically accessible. The internals of how an object carries out its job are

unimportant as long as that object can carry out that job. You are simply presented with an interface to the object, and you use that interface to make the object perform a given task on your behalf. (I'll further explain the concepts of encapsulation and interfaces later in this chapter.) The point here is that programs written to simulate the real-world objects of the problem domain are much easier to design and write because they allow us to think in a more natural way.

Notice that the second approach required an object to perform work on your behalf—that is, to total the detail lines. An object doesn't contain data only, as a structure does. Objects, by definition, comprise data and the methods that work on that data. This means that when working with a problem domain we can do more than design the necessary data structures. We can also look at which methods should be associated with a given object so that the object is a fully encapsulatedbit of functionality. The examples that follow here and in the coming sections help illustrate this concept.

NOTE

The code snippets in this chapter present the concepts of object-oriented programming. Keep in mind that while I present many example code snippets in C#, the concepts themselves are generic to OOP and are not specific to any one programming language. For comparison purposes in this chapter, I'll also present examples in C, which is not object-oriented.

(5)

Let's say you're writing an application to calculate the pay of your new company's only employee, Amy. Using C, you would code something similar to the following to associate certain data with an employee:

struct EMPLOYEE

{

char szFirstName[25];

char szLastName[25];

int iAge;

double dPayRate;

};

Here's how you'd calculate Amy's pay by using the EMPLOYEE structure:

void main()

{

double dTotalPay;

struct EMPLOYEE* pEmp;

pEmp = (struct EMPLOYEE*)malloc(sizeof(struct EMPLOYEE));

if (pEmp)

{

pEmp->dPayRate = 100;

strcpy(pEmp->szFirstName, "Amy");

strcpy(pEmp->szLastName, "Anderson");

pEmp->iAge = 28;

dTotalPay = pEmp->dPayRate * 40;

printf("Total Payment for %s %s is %0.2f",

pEmp->szFirstName, pEmp->szLastName, dTotalPay);

}

free(pEmp);

}

In this example, the code is based on data contained in a structure and some external (to that structure) code that uses that structure. So what's the problem? The main problem is one of abstraction: the user of the EMPLOYEE structure must know far too much about the data needed for an employee. Why? Let's say that at a later date you want to change how Amy's pay rate is calculated. For example, you might want to factor in FICA and other assorted taxes when determining a net payment. Not only would you have to change all client code that uses the EMPLOYEE structure, but you would also need to document—for any future programmers in your company—the fact that a change in usage had occurred.

(6)

class Employee

{

public Employee(string firstName, string lastName,

int age, double payRate)

{

this.firstName = firstName;

this.lastName = lastName;

this.age = age;

this.payRate = payRate;

}

protected string firstName;

protected string lastName;

protected int age;

protected double payRate;

public double CalculatePay(int hoursWorked)

{

// Calculate pay here.

return (payRate * (double)hoursWorked);

}

}

class EmployeeApp

{

public static void Main()

{

Employee emp = new Employee ("Amy", "Anderson", 28, 100);

Console.WriteLine("\nAmy's pay is $" + emp.CalculatePay(40));

}

}

In the C# version of the EmployeeApp example, the object's user can simply call the object's CalculatePay method to have the object calculate its own pay. The advantage of this approach is that the user no longer needs to worry about the internals of exactly how the pay is calculated. If at some time in the future you decide to modify how the pay is calculated, that modification will have no impact on existing code. This level of abstraction is one of the basic benefits of using objects.

Now, one valid comment might be that I could have abstracted the C client's code by creating a function to access the EMPLOYEE structure. However, the fact that I'd have to create this function completely apart from the structure being worked on is exactly the problem. When you use an object-oriented language such as C#, an object's data and the methods that operate on that data (its interface) are always together.

Keep in mind that only an object's methods should modify an object's variables. As you can see in the previous example, each Employee member variable is declared with the protected access modifier, except for the actual CalculatePay method, which is defined as public. Access modifiers are used to specify the level of access that derived class and client code has to a given class member. In the case of the protected modifier, a derived class would have

(7)

access to the member, but client code would not. The public modifier makes the member accessible to both derived classes and client code. I'll go into more detail on access modifiers in Chapter 5, "Classes," but the key thing to remember for now is that modifiers enable you to protect key class members from being used incorrectly.

Objects vs. Classes

The difference between a class and an object is a source of confusion for programmers new to the terminology of object-oriented programming. To illustrate the difference between these two terms, let's make our EmployeeApp example more realistic by assuming that we're working not with a single employee but with an entire company of employees.

Using the C language, we could define an array of employees—based on the EMPLOYEE structure—and start from there. Because we don't know how many employees our company might one day employ, we could create this array with a static number of elements, such as 10,000. However, given that our company currently has only Amy as its sole employee, this wouldn't exactly be the most efficient use of resources. Instead, we would normally create a linked list of EMPLOYEE structures and dynamically allocate memory as needed in our new payroll application.

My point is that we're doing exactly what we shouldn't be doing. We're expending mental energy thinking about the language and the machine—in terms of how much memory to allocate and when to allocate it—instead of

concentrating on the problem domain. Using objects, we can focus on the business logic instead of the machinery needed to solve the problem.

There are many ways to define a class and distinguish it from an object. You can think of a class as simply a type (just like char, int, or long) that has methods associated with it. An object is an instance of a type or class. However, the definition I like best is that a class is a blueprint for an object. You, as the developer, create this blueprint as an engineer would create the blueprint of a house. Once the blueprint is complete, you have only one blueprint for any given type of house. However, any number of people can purchase the blueprint and have the same house built. By the same token, a class is a blueprint for a given set of functionality, and an object created based on a particular class has all the functionality of the class built right in.

Instantiation

A term unique to object-oriented programming, instantiation is simply the act of creating an instance of a class. That instance is an object. In the following example, all we're doing is creating a class, or specification, for an object. In other words, no memory has been allocated at this time because we have only the blueprint for an object, not an actual object itself.

class Employee

{

public Employee(string firstName, string lastName,

int age, double payRate)

{

this.firstName = firstName;

this.lastName = lastName;

this.age = age;

this.payRate = payRate;

}

protected string firstName;

protected string lastName;

protected int age;

(8)

public double CalculatePay(int hoursWorked)

{

// Calculate pay here.

return (payRate * (double)hoursWorked);

}

}

To instantiate this class and use it, we have to declare an instance of it in a method similar to this:

public static void Main()

{

Employee emp = new Employee ("Amy", "Anderson", 28, 100);

}

In this example, emp is declared as type Employee and is instantiated using the new operator. The variable emp represents an instance of the Employee class and is considered an Employee object. After instantiation, we can communicate with this object through its public members. For example, we can call the emp object's CalculatePay method. We can't do this if we don't have an actual object. (There is one exception to this, and that's when we're dealing with static members. I'll discuss static members in both Chapter 5 and Chapter 6, "Methods.")

Have a look at the following C# code:

public static void Main()

{

Employee emp = new Employee();

Employee emp2 = new Employee();

}

Here we have two instances—emp and emp2—of the same Employee class. While programmatically each object has the same capabilities, each instance will contain its own instance data and can be treated separately. By the same token, we can create an entire array or collection of these Employee objects. Chapter 7, "Properties, Arrays, and Indexers," will cover arrays in detail. However, the point I want to make here is that most object-oriented languages support the ability to define an array of objects. This, in turn, gives you the ability to easily group objects and iterate through them by calling methods of the object array or by subscripting the array. Compare this to the work you'd have to do with a linked list, in which case you'd need to manually link each item in the list to the item that comes before and after it.

(9)

3

4

The Three Tenets of Object-Oriented Programming Languages

According to Bjarne Stroustrup, author of the C++ programming language, for a language to call itself object-oriented, it must support three concepts: objects, classes, and inheritance. However, object-oriented languages have come to be more commonly thought of as those languages built on the tripod of encapsulation, inheritance, and polymorphism. The reason for this shift in philosophy is that over the years we've come to realize that encapsulation and

polymorphism are just as integral to building object-oriented systems as class and inheritance.

Encapsulation

As I mentioned earlier, encapsulation, sometimes called information hiding, is the ability to hide the internals of an object from its users and to provide an interface to only those members that you want the client to be able to directly manipulate. However, I also spoke of abstraction in the same context, so in this section, I'll clear up any confusion regarding these two similar concepts. Encapsulation provides the boundary between a class's external interface—that is, the public members visible to the class's users—and its internal implementation details. The advantage of

encapsulation for the class developer is that he can expose the members of a class that will remain static, or unchanged, while hiding the more dynamic and volatile class internals. As you saw earlier in this chapter,

encapsulation is achieved in C# by virtue of assigning an access modifier—public, private, or protected—to each class member.

Designing Abstractions

An abstraction refers to how a given problem is represented in the program space. Programming languages themselves provide abstractions. Think about it like this: When was the last time you had to worry about the CPU's registers and stack? Even if you initially learned how to program in assembler, I'll bet it's been a long time since you had to worry about such low-level, machine specific details. The reason is that most programming languages abstract you from those details such that you can focus on the problem domain.

Object-oriented languages enable you to declare classes whose names and interfaces closely mimic real-world problem domain entities such that using theobjects have a more natural "feel" to them. The result of removing the elements not directly related to solving the problem at hand is that you're able to focus specifically on the problem and greater productivity. In fact, paraphrasing Bruce Eckel in Thinking in Java (Prentice Hall Computer Books, 2000), the ability to solve most problems will generally come down to the quality of the abstraction being used.

However, that's one level of abstraction. If you take that a step further, as a class developer you need to think in terms of how you can best design abstractions for your class's clients to allow the client to focus on the task at hand and not be mired in the details of how your class works. At this point, a good question might be, "How does a class's interface relate to abstraction?" The class's interface is the implementation of the abstraction.

I'll use a somewhat familiar analogy from programming courses to help crystallize these concepts: the internal workings of vending machines. The internals of a vending machine are actually quite involved. To fulfill its

responsibilities, the machine has to accept cash and coinage, make change, and dispense the selected item. However, the vending machine has a finite set of functions it needs to express to its users. This interface is expressed through a coin slot, buttons for selecting the desired item, a lever to request change, a slot that holds the returned change, and a shoot to dispense the selected item. Each of these items represents a part of the machine's interface. Vending

machines have, by and large, remained much the same since their invention. This is because despite the fact that the internals have changed as technology has evolved, the basic interface has not needed to change much. An integral part of designing a class's interface is having a deep enough understanding of the problem domain. This understanding will help you create an interface that gives the user access to the information and methods that they need yet

insulates them from the internal workings of the class. You need to design an interface not only to solve today's problems but also to abstract sufficiently from the class's internals so that private class members can undergo unlimited changes without affecting existing code.

(10)

comfortable with terms like cursors, commitment control, and tuples. However, most developers who haven't done a lot of database programming aren't going to be as knowledgeable about these terms. By using terms that are foreign to your class's clients, you have defeated the entire purpose of abstraction—to increase programmer productivity by representing the problem domain in natural terms.

Another example of when to think about the client would be when determining which class members should be publicly accessible. Once again, a little knowledge of the problem domain and your class's clients should make this obvious. In our database engine example, you'd probably not want your clients to be able to directly access members representing internal data buffers. How these data buffers are defined could easily change in the future. In addition, because these buffers are critical to the overall operation of your engine, you'd want to make sure that they are modified through your methods only. That way you can be assured that any necessary precautions are taken.

NOTE

You might think that object-oriented systems are designed primarily to make it easier to create classes. Although this feature does provide for short-term productivity gains, long-term gains come only after realizing that OOP exists to make programming easier for the class's clients. Always consider the programmer who is going to instantiate or derive from the classes that you create when designing your classes.

Benefits of Good Abstraction

Designing the abstraction of your classes in a way most useful to the programmers using them is paramount in developing reusable software. If you can develop a stable, static interface that persists across implementation changes, less of your application will need modification over time. For example, think of our earlier payroll example code. In the case of an Employee object and the payroll functionality, only a few methods are going to be relevant, such as CalculatePay, GetAddress, and GetEmployeeType. If you know the problem domain of a payroll application, you can easily determine, to a fairly high degree, the methods that the users of this class are going to need. Having said that, if you combine intimate knowledge of the problem domain with forethought and planning in the design of this class, you can be reasonably assured that the majority of your interface for this class will remain unchanged despite future changes in the actual implementation of the class. After all, from a user's perspective, it's only an Employee class. From the user's vantage point, almost nothing should change from version to version.

The decoupling of user and implementation detail is what makes an entire system easier to understand and therefore easier to maintain. Contrast this with procedural languages such as C, in which each module needs to explicitly name and access the members of a given structure. In that case, each time the structure's members change, every single line of code referring to the structure must also change.

Inheritance

Inheritance relates to the programmer's ability to specify that one class has a kind-of relationship with another class. Through inheritance, you can create (or derive) a new class that's based on an existing class. You can then modify the class the way that you want and create new objects of the derived type. This ability is the essence of creating a class hierarchy. Outside of abstraction, inheritance is the most significant part of a system's overall design. A derived class is the new class being created, and the base class is the one from which the new class is derived. The newly derived class inherits all the members of the base class, thereby enabling you to reuse previous work.

(11)

NOTE

In C#, the issue of which base class members are inherited is controlled by the access modifiers used to define the member . I'll get into that level of detail in Chapter 5. For the purposes of this discussion, you can assume that a derived class will inherit all its base class members.

As an example of when and how to use inheritance, let's look back at our EmployeeApp example. In that example, we would almost certainly have different types of employees, such as salaried, contractor, and hourly. While all of these Employee objects would have a similar interface, they would in many cases function differently internally. For instance, the CalculatePay method would work differently for a salaried employee than it would for a contractor. However, you want the same CalculatePay interface for your users regardless of employee type.

If you're new to object-oriented programming, you might be wondering, "Why do I even need objects here? Why can't I simply have an EMPLOYEE structure with an employee type member and then have a function similar to this?"

Double CalculatePay(EMPLOYEE* pEmployee, int iHoursWorked)

{

// Validate pEmployee pointer.

if (pEmployee->type == SALARIED)

{

// Do W-2 employee processing.

}

else if (pEmployee->type == CONTRACTOR)

{

// Do 1099 processing.

}

else if (pEmployee-> == HOURLY)

{

// Do hourly processing.

}

else

{

// Do corp-to-corp processing.

}

// Return the value from one of the

// compound statements above.

}

This code has a couple of problems. First, the success of the CalculatePay function is tightly linked to the EMPLOYEE structure. As I mentioned earlier, tight coupling like this is a problem because any modification to the EMPLOYEE structure will break this code. As an object-oriented programmer, the last thing you want to do is burden the users of your class with needing to know the intricate details of your class's design. That would be like a vending machine manufacturer requiring you to understand the internal mechanics of the vending machine before you can purchase a soda.

(12)

change anything necessary. Here's how that would look in C#:

class Employee

{

public Employee(string firstName, string lastName,

int age, double payRate)

{

this.firstName = firstName;

this.lastName = lastName;

this.age = age;

this.payRate = payRate;

}

protected string firstName;

protected string lastName;

protected int age;

protected double payRate;

public double CalculatePay(int hoursWorked)

{

// Calculate pay here.

return (payRate * (double)hoursWorked);

}

}

class SalariedEmployee : Employee

{

public string SocialSecurityNumber;

public void CalculatePay (int hoursWorked)

{

// Calculate pay for a W-2 employee.

}

}

class ContractEmployee : Employee

{

public string FederalTaxId;

public void CalculatePay (int hoursWorked)

{

// Calculate pay for a contract employee.

}

}

(13)

The base class, Employee, defines a string called EmployeeId that is inherited by both the SalariedEmployee

and the ContractEmployee classes. The two derived classes do nothing to get this member—they inherit it automatically as a by-product of being derived from the Employee class.

Both derived classes implement their own versions of CalculatePay. However, you'll notice that they both

inherited the interface, and although they changed the internals to suit their specific needs, the user's code remains the same.

● Both derived classes added members to the members that were inherited from the base class. The

SalariedEmployee class defines a SocialSecurityNumber string, and the ContractEmployee class includes a definition for a FederalTaxId member.

You've seen in this small example that inheritance enables you to reuse code by inheriting functionality from base classes. And it goes even further, allowing you to extend the class above and beyond that point by adding your own variables and methods.

Defining Proper Inheritance

To address the all-important issue of proper inheritance, I'll use a term from Marshall Cline and Greg Lomow's C++ FAQs (Addison-Wesley, 1998): substitutability.Substitutability means that the advertised behavior of the derived class is substitutable for the base class. Think about that statement for a moment—it's the single most important rule you'll learn regarding building class hierarchies that work. (By "work," I mean stand the test of time and deliver on the OOP promises of reusable and extendable code.)

Another rule of thumb to keep in mind when creating your class hierarchies is that a derived class should require no more and promise no less than its base class on any inherited interfaces. Not adhering to this rule breaks existing code. A class's interface is a binding contract between itself and programmers using the class. When a programmer has a reference to a derived class, the programmer can always treat that class as though it is the base class. This is called upcasting. In our example, if a client has a reference to a ContractEmployee object, it also has an implicit reference to that object's base, an Employee object. Therefore, by definition, ContractEmployee should always be able to function as its base class. Please note that this rule applies to base class functionality only. A derived class can choose to add behavior that is more restrictive regarding its requirements and promises as little as it wants. Therefore, this rule applies only to inherited members because existing code will have a contract with only those members.

Polymorphism

The best and most concise definition I've heard for polymorphism is that it is functionality that allows old code to call new code. This is arguably the biggest benefit of object-oriented programming because it allows you to extend or enhance your system without modifying or breaking existing code.

Let's say you write a method that needs to iterate through a collection of Employee objects, calling each object's CalculatePay method. That works fine when your company has one employee type because you can then insert the exact object type into the collection. However, what happens when you start hiring other employee types? For

example, if you have a class called Employee and it implements the functionality of a salaried employee, what do you do when you start hiring contract employees whose salaries have to be computed differently? Well, in a procedural language, you would modify the function to handle the new employee type, since old code can't possibly know how to handle new code. An object-oriented solution handles differences like this through polymorphism.

Using our example, you would define a base class called Employee. You then define a derived class for each employee type (as we've seen previously). Each derived employee class would then have its own implementation of the

CalculatePay method. Here's where the magic occurs. With polymorphism, when you have an upcasted pointer to an object and you call that object's method, the language's runtime will ensure that the correct version of the method is called. Here's the code to illustrate what I'm talking about:

(14)

class Employee

{

public Employee(string firstName, string lastName,

int age, double payRate)

{

this.firstName = firstName;

this.lastName = lastName;

this.age = age;

this.payRate = payRate;

}

protected string firstName;

protected string lastName;

protected int age;

protected double payRate;

public virtual double CalculatePay(int hoursWorked)

{

Console.WriteLine("Employee.CalculatePay");

return 42; // bogus value

}

}

class SalariedEmployee : Employee

{

public SalariedEmployee(string firstName, string lastName,

int age, double payRate)

: base(firstName, lastName, age, payRate)

{}

public override double CalculatePay(int hoursWorked)

{

Console.WriteLine("SalariedEmployee.CalculatePay");

return 42; // bogus value

}

}

class ContractorEmployee : Employee

{

public ContractorEmployee(string firstName, string lastName,

int age, double payRate)

: base(firstName, lastName, age, payRate)

{}

public override double CalculatePay(int hoursWorked)

{

(15)

return 42; // bogus value

}

}

class HourlyEmployee : Employee

{

public HourlyEmployee(string firstName, string lastName,

int age, double payRate)

: base(firstName, lastName, age, payRate)

{}

public override double CalculatePay(int hoursWorked)

{

Console.WriteLine("HourlyEmployee.CalculatePay");

return 42; // bogus value

}

}

class PolyApp

{

protected Employee[] employees;

protected void LoadEmployees()

{

Console.WriteLine("Loading employees...");

// In a real application, we'd probably get this

// from a database.

employees = new Employee[3];

employees[0] = new SalariedEmployee ("Amy", "Anderson", 28, 100);

employees[1] = new ContractorEmployee ("John", "Maffei", 35, 110);

employees[2] = new HourlyEmployee ("Lani", "Ota", 2000, 5);

Console.WriteLine("\n");

}

protected void CalculatePay()

{

foreach(Employee emp in employees)

{

emp.CalculatePay(40);

}

}

public static void Main()

{

(16)

}

}

Compiling and running this application will yield the following results:

c:\>PolyApp

Loading employees...

SalariedEmployee.CalculatePay

ContractorEmployee.CalculatePay

HourlyEmployee.CalculatePay

Note that polymorphism provides at least two benefits. First, it gives you the ability to group objects that have a common base class and treat them consistently. In the example above, although technically I have three different object types—SalariedEmployee, ContractorEmployee,and HourlyEmployee—I can treat them all as Employee objects because they all derive from the Employee base class. This is how I can stuff them in an array that is defined as an array of Employee objects. Because of polymorphism, when I call one of those object's methods, the runtime will ensure that the correct derived object's method is called.

The second advantage is the one I mentioned at the beginning of this section: old code can use new code. Notice that the PolyApp.CalculatePay method iterates through its member array of Employee objects. Because this method extracts the objects as implicitly upcasted Employee objects and the runtime's implementation of polymorphism ensures that the correct derived class's method is called, I can add other derived employee types to the system, insert them into the Employee object array, and all my code continues working without me having to change any of my original code!

(17)

3

4

Summary

This chapter has taken you on a whirlwind tour through terminology and concepts that fall under the umbrella of object-oriented programming. A full discourse on the subject would require several chapters and would, therefore, take away from the focus of this book. However, a firm grasp of object-oriented fundamentals is imperative to getting the most out of the C# language.

We covered quite a few ideas in this chapter. Key to understanding object-oriented systems is knowing the difference between classes, objects, and interfaces and how these concepts relate to effective solutions. Good object-oriented solutions also depend on a sound implementation of the three tenets of object-oriented programming: encapsulation, inheritance, and polymorphism. The concepts presented in this chapter lay the groundwork for the next chapter, which introduces the Microsoft .NET Framework and the Common Language Runtime.

(18)

2

Introducing Microsoft .NET

Without a firm understanding of .NET and how the C# language plays into this Microsoft initiative, you won't fully comprehend some of the core elements of C# that are supported by the .NET runtime. The .NET overview presented in this chapter will help you understand not only the terminology used throughout this book, but also why certain

features of the C# language work the way they do.

If you read any newsgroup or mailing list on the subject of .NET, you can see that some users are confused by the terminology of the technology. With the ambiguous and sometimes contradictory names being tossed about, it's been difficult just to keep track of the players. Obviously, the fact that all this is very new is part of the problem. The first thing I'd like to do is explain some terminology with regards to .NET.

(19)

3

4

The Microsoft .NET Platform

The idea behind Microsoft .NET is that .NET shifts the focus in computing from a world in which individual devices and Web sites are simply connected through the Internet to one in which devices, services, and computers work together to provide richer solutions for users. The Microsoft .NET solution comprises four core components:

● .NET Building Block Services, or programmatic access to certain services, such as file storage, calendar, and

Passport.NET (an identity verification service).

● .NET device software, which will run on new Internet devices.

● The .NET user experience, which includes such features as the natural interface, information agents, and smart

tags, a technology that automates hyperlinks to information related to words and phrases in user-created documents.

● The .NET infrastructure, which comprises the .NET Framework, Microsoft Visual Studio.NET, the .NET

Enterprise Servers, and Microsoft Windows.NET.

The .NET infrastructure is the part of .NET that most developers are referring to when they refer to .NET. You can assume that any time I refer to .NET (without a preceding adjective) I'm talking about the .NET infrastructure. The .NET infrastructure refers to all the technologies that make up the new environment for creating and running robust, scalable, distributed applications. The part of .NET that lets us develop these applications is the .NET Framework. The .NET Framework consists of the Common Language Runtime (CLR) and the .NET Framework class libraries, sometimes called the Base Class Library (BCL). Think of the CLR as the virtual machine in which .NET applications function. All .NET languages have the .NET Framework class libraries at their disposal. If you're familiar with either the Microsoft Foundation Classes (MFC) or Borland's Object Windows Library (OWL), you're already familiar with class libraries. The .NET Framework class libraries include support for everything from file I/O and database I/O to XML and SOAP. In fact, the .NET Framework class libraries are so vast that it would easily take a book just to give a superficial overview of all the supported classes.

As a side note (as well as an admission of my age), when I use the term "virtual machine," I don't mean the Java Virtual Machine (JVM). I'm actually using the traditional definition of the term. Several decades ago, before Java was anything more than another word for a dark, hot beverage, IBM first coined "virtual machine." A virtual machine was a high-level operating system abstraction within which other operating systems could function in a completely

encapsulated environment. When I refer to the CLR as a kind of virtual machine, I'm referring to the fact that the code that runs within the CLR runs in an encapsulated and managed environment, separate from other processes on the machine.

(20)

The .NET Framework

Let's look at what the .NET Framework is and what it delivers. The first thing I'll do is compare .NET to an earlier distributed application development environment. I'll then go through a list of the capabilities that .NET provides application developers to create powerful distributed applications more quickly.

Windows DNA and .NET

Did the phrase that I used earlier to describe .NET— "the new environment for creating and running robust, scalable, distributed

applications"—sound familiar? If so, there's a reason: .NET is basically the offspring of an earlier attempt to satisfy these lofty goals. That platform was called Windows DNA. However, .NET is much more than Windows DNA was meant to be. Windows DNA was a solutions platform that focused on solving business problems through the use of Microsoft server products. The term "glue" was sometimes used with Windows DNA, as in, "DNA defines the glue used to piece together robust, scalable, distributed systems." However, aside from being a technical specification, Windows DNA didn't have any tangible pieces. This is just one of several major differences between Windows DNA and .NET. Microsoft .NET is not just a set of specifications. It also includes several tangible products, such as compilers, class libraries, and even whole end-user applications.

The Common Language Runtime

The CLR is the very core of .NET. As the name suggests, it is a run-time environment in which applications written in different languages can all play and get along nicely—otherwise known as cross-language interoperability. How does the CLR provide this cozy environment for cross-language interoperability? The Common Language Specification (CLS) is a set of rules that a language compiler must adhere to in order to create .NET applications that run in the CLR. Anyone, even you or me, who wants to write a .NET-compliant compiler needs simply to adhere to these rules and, voila!, the applications generated from our compilers will run right alongside any other .NET application and have the same interoperability.

An important concept related to the CLR is managed code. Managed code is just code that is running under the auspices of the CLR and is therefore being managed by the CLR. Think of it like this: In today's Microsoft Windows environments, we have disparate processes running. The only rule that applications are required to follow is that they behave well in the Windows environment. These applications are created by using one of a multitude of completely dissimilar compilers. In other words, the applications have to obey only the most general of rules to run under Windows. The Windows environment has few global rules regarding how the applications must behave in terms of communicating with one another, allocating memory, or even enlisting the Windows operating system to do work on their behalf. However, in a managed code environment, a number of rules are in place to ensure that all applications behave in a globally uniform manner, regardless of the language they were written in. The uniform behavior of .NET applications is the essence of .NET and can't be overstated. Luckily for you and me, these global rules primarily affect the compiler writers.

The .NET Framework Class Libraries

The .NET Framework class libraries are monumentally important to providing language interoperability because they allow developers to use a single programming interface to all the functionality exposed by the CLR. If you've ever used more than one dissimilar language in development for Windows, you'll love this feature. In fact, the .NET Framework class libraries are forging a revolutionary trend in compiler development. Before .NET, most compiler writers developed a language with the ability to do most of its own work. Even a language such as C++, which was designed as a scaled-down grouping of functionality to be used in conjunction with a class library, has at least some functionality on its own. However, in the world of .NET, languages are becoming little more than syntactical interfaces to the .NET Framework class libraries.

As an example, let's first take a look at the standard "Hello, World" application in C++ and then compare it to an application that does the same thing in C#:

#include <iostream.h>

int main(int argc, char* argv[]) {

cout << "Hello, World!" << endl; return 0;

}

Notice that the application first includes a header file with the declaration of the cout function. The application's main function—every C/C++ application's entry point—uses the cout function to write the string "Hello, World" to the standard output device. However, what's important to note here is that you can't write this application in any .NET language without the .NET Framework class libraries. That's right: .NET languages don't even have the most basic compiler features, such as the ability to output a string to the console. Now I know that technically the cout function is implemented in the C/C++ runtime, which is itself a library. However, basic C++ tasks such as string formatting, file I/O, and screen I/O are at least logically considered part of the base language. With C#—or any .NET language for that matter—the language itself has almost no ability to do even the most menial task without the .NET Framework class library.

(21)

Let's look at the "Hello, World" example in C# to see what I mean:

using System; class Hello {

public static void Main() {

Console.WriteLine("Hello, World"); }

}

So, what does this common set of class libraries mean to you, and is it a good thing? Well, it depends on your vantage point. A common set of class libraries means that all languages, theoretically, have the same capabilities because they all have to use these class libraries to accomplish anything except declaring variables.

One gripe I've seen on discussion boards is, "Why have multiple languages if they all have the same capabilities?" For the life of me, I don't understand this complaint. As someone that has worked in many multilanguage environments, I can attest that there's a great benefit to not having to remember what language can do what with the system and how it does it. After all, our job as developers is to produce code, not to worry about whether a favorite language has this advantage or that advantage.

Another question I've seen frequently is, "If all these .NET languages can do the same thing, why do we need more than one?" The answer relates to the fact that programmers are creatures of habit. Microsoft certainly didn't want to pick one language out of the many available and force millions of programmers to toss out their years of experience in other languages. Not only might a programmer have to become familiar with a new API, he or she might have to master a completely different syntax. Instead, a developer can continue using the language that's best suited for the job. After all, the name of the game is productivity. Changing what doesn't need to be changed is not part of that equation.

NOTE

While in theory the .NET Framework class libraries enable compilers to make all the CLR's functionality available to a language's users, this is not always the case. One point of contention at Microsoft between the .NET Framework class libraries team and the different compiler teams is that although the .NET Framework class libraries team has attempted to expose all its functionality to the different languages, there's nothing—besides meeting minimal CLS standards—that requires the different compiler teams to implement every single feature. When I asked several Microsoft developers about this discrepancy, I was told that instead of each language having access to every exposed bit of .NET Framework functionality, each compiler team has decided to implement only the features that they feel are most applicable to their users. Luckily for us, however, C# happens to be the language that seems to have provided an interface to almost all of the .NET Framework functionality.

Microsoft Intermediate Language and the JITters

To make it easy for language writers to port their languages to .NET, Microsoft developed a language akin to assembly language called Microsoft intermediate language (MSIL). To compile applications for .NET, compilers take source code as input and produce MSIL as output. MSIL itself is a complete language that you can write applications in. However, as with assembly language, you would probably never do so except in unusual circumstances. Because MSIL is its own language, each compiler team makes its own decision about how much of the MSIL it will support.

However, if you're a compiler writer and you want to create a language that does interoperate with other languages, you should restrict yourself to features specified by the CLS.

When you compile a C# application or any application written in a CLS-compliant language, the application is compiled into MSIL. This MSIL is then further compiled into native CPU instructions when the application is executed for the first time by the CLR. (Actually, only the called functions are compiled the first time they are invoked.) However, since we're all geeks here and this book is called Inside C#, let's look at what's really

happening under the hood:

1. You write source code in C#.

2. You then compile it using the C# compiler (csc.exe) into an EXE.

3. The C# compiler outputs the MSIL code and a manifest into a read-only part of the EXE that has a standard PE (Win32-portable executable) header.

So far, so good. However, here's the important part: when the compiler creates the output, it also imports a function named _ CorExeMain from the .NET runtime.

(22)

5. The operating system loader then jumps to the entry point inside the PE, which is put there by the C# compiler. Once again, this is exactly how any other PE is executed in Windows.

However, since the operating system obviously can't execute the MSIL code, the entry point is just a small stub that jumps to the _ CorExeMain function in mscoree.dll.

6. The _ CorExeMain function starts the execution of the MSIL code that was placed in the PE.

7. Since MSIL code cannot be executed directly—because it's not in a machine-executable format—the CLR compiles the MSIL by using a just-in-time (JIT) compiler (or JITter) into native CPU instructions as it processes the MSIL. JIT compiling occurs only as methods in the program are called. The compiled executable code is cached on the machine and is recompiled only if there's some change to the source code. Three different JITters can be used to convert the MSIL into native code, depending on the circumstances:

Install-time code generation Install-time code generation will compile an entire assembly into CPU-specific binary code, just as a C++

compiler does. An assembly is the code package that's sent to the compiler. (I'll talk about assemblies in more detail later in this chapter in "Deployment.") This compilation is done at install time, when the end user is least likely to notice that the assembly is being JIT-compiled. The advantage of install-time code generation is that it allows you to compile the entire assembly just once before you run it. Because the entire assembly is compiled, you don't have to worry about intermittent performance issues every time a method in your code is executed the first time. It's like a time-share vacation plan in which you pay for everything up front. While paying for the vacation plan is painful, the advantage is that you never have to worry about paying for accommodations again. When and if you use this utility depends on the size of your specific system and your deployment environment. Typically, if you're going to create an installation application for your system, you should go ahead and use this JITter so that the user has a fully optimized version of the system "out of the box."

JIT The default JITter is called at run time—in the manner I described in the preceding numbered list—each time a method is invoked for

the first time. This is akin to a "pay-as-you-go" plan and is the default if you don't explicitly run the PreJIT compiler.

EconoJIT Another run-time JITter, the EconoJIT is specifically designed for systems that have limited resources—for example, handheld

devices with small amounts of memory. The major difference between this JITter and the regular JITter is the incorporation of something called code pitching. Code pitching allows the EconoJIT to discard the generated, or compiled, code if the system begins to run out of memory. The benefit is that the memory is reclaimed. However, the disadvantage is that if the code being pitched is invoked again, it must be compiled again as though it had never been called.

Unified Type System

One of the key features of any development environment is its type system. After all, a development environment with a limited amount of types or a system that limits the programmer's ability to extend the system-supplied types isn't an environment with a long life expectancy. The .NET runtime does more than just give the developer a single, unified type system that is used across all CLS-compliant languages. It also lets language writers extend the type system by adding new types that look and act just like the system built-in types. This means that you, as a developer, can use all typesin a uniform manner, regardless of whether they are .NET predefined types or user-created types. I'll discuss the details of the type system and how the C# compiler supports it in Chapter 4, "The Type System."

Metadata and Reflection

As I mentioned in the earlier section "Microsoft Intermediate Language and the JITters," the CLS-compliant compilers take your source code as input and produce MSIL code for the runtime to compile (via the JITters) and execute. In addition to mapping source code to MSIL instruction sequences, CLS-compliant compilers have another equally important task: embedding metadata into the resulting EXE.

Metadata is data that describes data. In this context, metadata is the collection of programmatic items that constitute the EXE, such as the types declared and the methods implemented. If this sounds vaguely familiar, it should. This metadata is similar to the type libraries (typelibs) generated with Component Object Model (COM) components. Not only is the metadata output from a .NET compiler substantially more expressive and complete than the COM typelibs we're accustomed to, but the metadata is also always embedded in the EXE. This way, there's no chance of losing the application's metadata or having a mismatched pair of files.

The reason for using metadata is simple. It allows the .NET runtime to know at run time what types will be allocated and what methods will be called. This enables the runtime to properly set up its environment to more efficiently run the application. The means by which this metadata is queried is called reflection. In fact, the .NET Framework class libraries provide an entire set of reflection methods that enable any application—not just the CLR—to query another application's metadata.

Tools such as Visual Studio.NET use these reflection methods to implement features such as IntelliSense. With IntelliSense, as you type in a method name, that method's arguments pop up in a list box on the screen. Visual Studio.NET takes that functionality even further, showing all the members of a type. I'll discuss the reflection APIs in Chapter 15, "Multithreaded Programming."

(23)

utility parses the target application's metadata and then displays information about the application in a treelike hierarchy. Figure 2-1 illustrates what the "Hello, World" C# application looks like in ILDASM.

Figure 2-1 The C# "Hello, World" application displayed in ILDASM.

The window in the background of Figure 2-1 is the main IL Disassembler window. Double-clicking the Main method in the tree view brings up the window in the foreground that shows the Main method's details.

Security

The most important facet of any distributed application development environment is how it handles security. Thankfully for those of us who have long complained that Microsoft would never be taken seriously in the server-side enterprise solutions space without a completely new approach to security, .NET brings many concepts to the table. In fact, security begins as soon as a class is loaded by the CLR because the class loader is a part of the .NET security scheme. For example, when a class is loaded in the .NET runtime, security-related factors such as accessibility rules and self-consistency requirements are verified. In addition, security checks ensure that a piece of code has the proper credentials to access certain

resources. Security code ensures role determination and identity information. These security checks even span process and machine boundaries to ensure that sensitive data is not compromised in distributed computing environments.

(24)

where the .NET design team obviously spent a lot of time.

The key to .NET application deployment is the concept of assemblies. Assemblies are simply packages of semantically related behavior that are built as either single-file or multiple-file entities. The specifics of how you deploy yourapplication will vary based on whether you're developing a Web server application or a traditional desktop application for Windows. However, with the introduction of the assembly as a fully encapsulated set of functionality, deployment can be as simple as copying the necessary assemblies to a target location.

Many of the problems that caused so much trouble for programmers prior to the .NET Framework have now been eliminated. For example, there's no need to register components—as you do with COM components and Microsoft ActiveX controls—because with metadata and reflection, all components are self-describing. The .NET run time also keeps track of the files, and the versions of the files, associated with an application. Therefore, any application that is installed is automatically associated with the files that are part of its assembly. If a setup application attempts to overwrite a file needed by another application, the .NET runtime is smart enough to allow the setup application to install the needed file, but the CLR doesn't delete the previous version of the file because it's still required by the first application.

Interoperability with Unmanaged Code

As you might suspect, unmanaged code is code that isn't controlled by the .NET runtime. Let's be clear about something: this code is still run by the .NET runtime. However, unmanaged code doesn't have the advantages that managed code has, such as garbage collection, a unified type system, and metadata. You might wonder why anyone would want to run unmanaged code in the .NET environment. Well, you wouldn't do it out of choice. Rather, you would do it when faced with circumstances that offer few alternatives. Here are some situations that will make you thankful Microsoft put this feature into .NET:

Managed code calling unmanaged DLL functions Let's say your application needs to interface to a C-like DLL and the company that

wrote the DLL isn't adopting .NET as quickly as your company is. In this case, you still need to call into that DLL from a .NET application. I'll cover this very example in Chapter 16, "Querying Metadata with Reflection."

Managed code using COM components For the same reason that you might need to continue supporting the ability to call a C-like DLL's

functions from your .NET application, you might also need to continue supporting COM components. You do this by creating a .NET wrapper for the COM component so that the managed client thinks it's working with a .NET class. This is also covered in Chapter 16.

Unmanaged code using .NET services This is exactly the reverse problem—you want to access .NET from unmanaged code. It's solved

using a reciprocal approach: a COM client is fooled into thinking it's using a COM server, which is actually a .NET service of some sort. You'll also see examples of this in Chapter 16.

(25)

3

4

Summary

Microsoft .NET represents a shift to a computing model in which devices, services, and computers work together to provide solutions for users. Central to that shift is the development of the .NET Framework and the CLR, shown in Figure 2-2 on the following page. The .NET Framework contains the class libraries shared by the languages compiled to run in the CLR. Because C# was designed for the CLR, you can't perform even the simplest tasks in C# without the CLR and .NET Framework class libraries. Understanding the features of these technologies is necessary to get the most out of C# and the remainder of this book.

Figure 2-2 The .NET Framework contains libraries designed to facilitate interoperation between services, devices, and

(26)

3

Hello, C#

Before we get to the heart of our subject—Part II, "C# Class Fundamentals," and Part III, "Writing Code"—I thought it would be a good idea to have a "getting started" chapter. In this chapter, I'll take you on a quick tour of the

development process for a simple C# application. First, I'll cover the advantages and disadvantages of the different editors you can use to write in C#. Once you've selected an editor, we'll write the canonical "Hello, World" example application to get to know the basic syntax and structure of writing C# applications. You'll see that as in most

languages, this syntax is formulaic and you can use this application as a template to write most basic C# applications. You'll then learn how to compile using the command-line compiler, and you'll learn how to run your new application.

(27)

3

4

Writing Your First C# Application

Let's go through the steps to get your first C# application up and running.

Choosing an Editor

Before you write a C# application, you need to choose an editor. The following sections describe the most prevalent editors and offer some pertinent facts about choosing an editor for your C# development.

Notepad

Microsoft Notepad has been the most commonly used editor during the early stages of C# for developers using the .NET Framework SDK to write C# applications. I used Notepad for this book for some other reasons I'll outline later. However, I wouldn't recommend using Notepad for the reasons described.

C# files should be saved with an extension of .cs. However, in Notepad's File Save dialog box, if you're not careful, attempting to name your file something like Test.cs will result in a file named test.cs.txt unless you remember to change the setting in the Save As Type drop-down list box to "All Files."

● Notepad doesn't display line numbers—a major pain when the compiler reports an error on a given line.

● Notepad inserts eight spaces for a tab, which means that writing anything beyond "Hello, World" can make applications difficult to read.

● Notepad does not perform automatic indenting when you press the Enter key. Therefore, you have to manually tab over to the desired column to enter a line of code.

Other reasons exist, but as you can see, Notepad is not a good choice for developing C# applications.

Visual Studio 6

Because my background in development for Microsoft Windows is entrenched in the Microsoft Visual C++ language, Microsoft Visual Studio 6 is naturally my editor of choice. Visual Studio is a full-featured editor that includes all the features necessary to edit and save C# files.

One of the biggest advantages of using a programmer's editor is syntax highlighting. However, because Visual Studio 6 was released a couple of years before C# and was designed to develop Visual C++ applications, you'll need to do a little tweaking for Visual Studio 6 to highlight C# code correctly. The first step is to change a Visual Studio Registry key. Locate the following key in your Registry by using Regedit.exe or another Registry editor:

HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\

Text Editor\Tabs/Language Settings\C/C++\FileExtensions

The value will contain a string like the following:

cpp;cxx;c;h;hxx;hpp;inl;tlh;tli;rc;rc2

Add the .cs extension at the end of the value. (Note that an ending semicolon is optional.) The new Registry value would look like this:

(28)

Next you need to tell Visual Studio which files are keywords in C#. To do this, you create and place in the same folder as the msdev.exe file a file named usertype.dat. This is an ASCII text file that contains all the keywords that should be highlighted—place one keyword on each line. When Visual Studio starts up, it will load this file. Therefore, when you make a change to this file, you must restart Visual Studio to see your changes. I've included a copy of a usertype.dat file on this book's companion CD that lists all the keywords for the C# language. Figure 3-1 shows what your C# code will look like once you've followed these steps.

Figure 3-1 One advantage of using an editor that provides syntax highlighting like the one in Visual Studio 6 is immediate feedback

as to whether a keyword is valid.

Visual Studio.NET

Obviously, if you want the maximum in productivity in the .NET environment, you should use Visual Studio.NET. Not only does it provide all the integrated tools and wizards for creating C# applications, but it also includes productivity features such as IntelliSense

References

Related documents

We have, for example, instruction manuals listing codes of conduct (e.g. what to wear and say on ritual occasions, what emotions to convey) alongside treatises explaining the

All of the participants were faculty members, currently working in a higher education setting, teaching adapted physical activity / education courses and, finally, were

Rectangular, Exposed Breeching and Connector Insulation: high-temperature mineral-fiber board.. Rectangular, Concealed Breeching and Connector Insulation

Other anatomic points such as lingula, the left and right anterior clinoid processes, and genial tubercles were chosen because they could easily be located using a three

This paper is an introduction to frame the conversation about the “new” personalization – and how delivering content as a contextualized, and personalized platform is really a

Said malik comes to one you re the one direction at a solo career, for liam payne admitted the best results, indicating different place in this.. Hard for reflection, this

RX330 offers enough local storage capacity for use as a stand-alone application server and for server farms in scale-out environments with average requirements regarding

antennal fossae separated by about width of pedicel; pronotum about as long as middle width, with at least one elongate white seta laterally; mesonotum and mesoscutellum usually