Thư viện tri thức trực tuyến
Kho tài liệu với 50,000+ tài liệu học thuật
© 2023 Siêu thị PDF - Kho tài liệu học thuật hàng đầu Việt Nam

Programming C# 4.0 phần 2 ppt
Nội dung xem thử
Mô tả chi tiết
Defining Classes
We can start out with the simplest possible class. It will have no methods, and no data,
so as a model of a plane in our system, it leaves something to be desired, but it gets us
started.
If you want to build your own version as you read, create a new Console Application
project just as we did in Chapter 2. To add a new class, use the Project→Add Class
menu item (or right-click on the project in the Solution Explorer and select Add→Class).
It’ll add a new file for the class, and if we call it Plane.cs, Visual Studio will create a new
source file with the usual using directives and namespace declaration. And most importantly, the file will contain a new, empty class definition, as shown in Example 3-1.
Example 3-1. The empty Plane class
class Plane
{
}
Right; if we look back at the specification, there’s clearly a whole bunch of information
we’ve got about the plane that we need to store somewhere. C# gives us a handy
mechanism for this called a property.
Representing State with Properties
Each plane has an identifier which is just a string of letters and numbers. We’ve already
seen a built-in type ideal for representing this kind of data: string. So, we can add a
property called Identifier, of type string, as Example 3-2 shows.
Example 3-2. Adding a property
class Plane
{
string Identifier
{
get;
set;
}
}
A property definition always states the type of data the property holds (string in this
case), followed by its name. By convention, we use PascalCasing for this name—see
the sidebar on the next page. As with most nontrivial elements of a C# program, this
is followed by a pair of braces, and inside these we say that we want to provide a getter and a set-ter for the property. You might be wondering why we need to declare
these—wouldn’t any property need to be gettable and settable? But as we’ll see, these
explicit declarations turn out to be useful.
64 | Chapter 3: Abstracting Ideas with Classes and Structs
PascalCasing and camelCasing
Most programming languages, including C#, use whitespace to separate elements of
the code—it must be clear where one statement (or keyword, variable, or whatever)
ends and the next begins, and we often rely on spaces to mark the boundaries. But this
gives us a problem when it comes to naming. Lots of features of a program have
names—classes, methods, properties, and variables, for example—and we might want
to use multiple words in a name. But we can’t put a space in the middle of a name like
this:
class Jumbo Jet
{
}
The C# compiler would complain—the space after Jumbo marks the end of the name,
and the compiler doesn’t understand why we’ve put a second name, Jet, after that. If
we want to use multiple words in a name, we have to do it without using spaces. C#
programmers conventionally use two styles of capitalization to put multiple words in
a name:
• PascalCasing, where each word starts with a capital letter. This is used for types,
properties, and methods.
• camelCasing, where the first word starts with a lowercase letter and all subsequent
words get a capital. This is used for parameters and fields.
Pascal casing takes its name from the fact that it was a popular style among Pascal
programmers. It’s not a widely used language today, but lots of developers cut their
teeth on it a decade or three ago when drainpipe trousers, trilby hats, and black-andwhite print T-shirts were the latest in fashion (or at least, they were in parts of Europe).
And, by no coincidence whatsoever, Anders Hejlsberg (a key figure in the C# design
team) also designed Borland’s Turbo Pascal.
As for camel casing, that name comes from the fact that uppercase letters only ever
appear in the middle of the name, meaning you get one or more humps in the middle,
like a camel.
There’s a wrinkle in these conventions. Acronyms generally get treated as though they
are words, so if you had a class for an RGB color you might call it ColorRgb, and a color
with an alpha channel might be ColorArgb. (The .NET Framework class libraries include
types that refer to Argb, and people often mistakenly think that the “Arg” is short for
“argument” rather than Alpha, Red, Green, and Blue.)
There’s an exception to this exception: two-letter acronyms are usually capitalized. So
a person’s intelligence quotient might be recorded as PersonIQ.
These naming conventions are optional, but strongly recommended to help people
understand your code. MSDN offers an extensive set of guidelines for these sorts of
conventions at http://msdn.microsoft.com/library/ms229042.
Defining Classes | 65
If we create an instance of this class, we could use this Identifier property to get and
set its identifier. Example 3-3 shows this in a modified version of the Main function in
our Program.cs file.
Example 3-3. Using the Plane class’s property
static void Main(string[] args)
{
Plane someBoeing777 = new Plane();
someBoeing777.Identifier = "BA0049";
Console.WriteLine(
"Your plane has identifier {0}",
someBoeing777.Identifier);
// Wait for the user to press a key, so
// that we can see what happened
Console.ReadKey();
}
But wait! If you try to compile this, you end up with an error message:
'Plane.Identifier' is inaccessible due to its protection level
What’s that all about?
Protection Levels
Earlier, we mentioned that one of the objectives of good design is encapsulation: hiding
the implementation details so that other developers can use our objects without relying
on (or knowing about) how they work. As the error we just saw in Example 3-3 shows,
a class’s members are hidden by default. If we want them to be visible to users of our
class, we must change their protection level.
Every entity that we declare has its own protection level, whether we specify it or not.
A class, for example, has a default protection level called internal. This means that it
can only be seen by other classes in its own assembly. We’ll talk a lot more about
assemblies in Chapter 15. For now, though, we’re only using one assembly (our example application itself), so we can leave the class at its default protection level.
While classes default to being internal, the default protection level for a class member
(such as a property) is private. This means that it is only accessible to other members
of the class. To make it accessible from outside the class, we need to change its protection level to public, as Example 3-4 shows.
Example 3-4. Making a property public
class Plane
{
public string Identifier
{
66 | Chapter 3: Abstracting Ideas with Classes and Structs
get;
set;
}
}
Now when we compile and run the application, we see the correct output:
Your plane has identifier BA0049
Notice how this is an opt-in scheme. If you don’t do anything to the contrary, you get
the lowest sensible visibility. Your classes are visible to any code inside your assembly,
but aren’t accessible to anyone else; a class’s properties and methods are only visible
inside the class, unless you explicitly choose to make them more widely accessible.
When different layers specify different protection, the effective accessibility is the lowest specified. For example, although our property has public accessibility, the class of
which it is a member has internal accessibility. The lower of the two wins, so the
Identifier property is, in practice, only accessible to code in the same assembly.
It is a good practice to design your classes with the smallest possible public interface
(part of something we sometimes call “minimizing the surface area”). This makes it
easier for clients to understand how they’re supposed to be used and often cuts down
on the amount of testing you need to do. Having a clean, simple public API can also
improve the security characteristics of your class framework, because the larger and
more complex the API gets, the harder it generally gets to spot all the possible lines of
attack.
That being said, there’s a common misconception that accessibility modifiers “secure”
your class, by preventing people from accessing private members. Hence this warning:
It is important to recognize that these protection levels are a convenient
design constraint, to help us structure our applications properly. They
are not a security feature. It’s possible to use the reflection features described in Chapter 17 to circumvent these constraints and to access these
supposedly hidden details.
To finish this discussion, you should know that there are two other protection levels
available to us—protected and protected internal—which we can use to expose (or
hide) members to developers who derive new classes from our class without making
the members visible to all. But since we won’t be talking about derived classes until
Chapter 4, we’ll defer the discussion of these protection levels until then.
We can take advantage of protection in our Plane class. A plane’s identifier shouldn’t
change mid-flight, and it’s a good practice for code to prevent things from happening
that we know shouldn’t happen. We should therefore add that constraint to our class.
Fortunately, we have the ability to change the accessibility of the getter and the setter
individually, as Example 3-5 shows. (This is one reason the property syntax makes use
declare the get and set explicitly—it gives us a place to put the protection level.)
Defining Classes | 67
Example 3-5. Making a property setter private
class Plane
{
public string Identifier
{
get;
private set;
}
}
Compiling again, we get a new error message:
The property or indexer 'Plane.Identifier' cannot be used in this context because
the set accessor is inaccessible
The problem is with this bit of code from Example 3-3:
someBoeing777.Identifier = "BA0049";
We’re no longer able to set the property, because we’ve made the setter private (which
means that we can only set it from other members of our class). We wanted to prevent
the property from changing, but we’ve gone too far: we don’t even have a way of giving
it a value in the first place. Fortunately, there’s a language feature that’s perfect for this
situation: a constructor.
Initializing with a Constructor
A constructor is a special method which allows you to perform some “setup” when you
create an instance of a class. Just like any other method, you can provide it with parameters, but it doesn’t have an explicit return value. Constructors always have the
same name as their containing class.
Example 3-6 adds a constructor that takes the plane’s identifier. Because the constructor is a member of the class, it’s allowed to use the Identifier property’s private setter.
Example 3-6. Defining a constructor
class Plane
{
public Plane(string newIdentifier)
{
Identifier = newIdentifier;
}
public string Identifier
{
get;
private set;
}
}
68 | Chapter 3: Abstracting Ideas with Classes and Structs
Notice how the constructor looks like a standard method declaration, except that since
there’s no need for a return type specifier, we leave that out. We don’t even write
void, like we would for a normal method that returns nothing. And it would be weird
if we did; in a sense this does return something—the newly created Plane—it just does
so implicitly.
What sort of work should you do in a constructor? Opinion is divided on the subject—
should you do everything required to make the object ready to use, or the minimum
necessary to make it safe? The truth is that it is a judgment call—there are no hard and
fast rules. Developers tend to think of a constructor as being a relatively low-cost operation, so enormous amounts of heavy lifting (opening files, reading data) might be a
bad idea. Getting the object into a fit state for use is a good objective, though, because
requiring other functions to be called before the object is fully operational tends to lead
to bugs.
We need to update our Main function to use this new constructor and to get rid of the
line of code that was setting the property, as Example 3-7 shows.
Example 3-7. Using a constructor
static void Main(string[] args)
{
Plane someBoeing777 = new Plane("BA0049");
Console.WriteLine(
"Your plane has identifier {0}",
someBoeing777.Identifier);
Console.ReadKey();
}
Notice how we pass the argument to the constructor inside the parentheses, in much
the same way that we pass arguments in a normal method call.
If you compile and run that, you’ll see the same output as before—but now we have
an identifier that can’t be changed by users of the object.
Be very careful when you talk about properties that “can’t be changed”
because they have a private setter. Even if you can’t set a property, you
may still be able to modify the state of the object referred to by that
property. The built-in string type happens to be immune to that because it is immutable (i.e., it can’t be changed once it has been created),
so making the setter on a string property private does actually prevent
clients from changing the property, but most types aren’t like that.
Speaking of properties that might need to change, our specification requires us to know
the speed at which each plane is traveling. Sadly, our specification didn’t mention the
units in which we were expected to express that speed. Let’s assume it is miles per hour,
Defining Classes | 69
and add a suitable property. We’ll use the floating-point double data type for this.
Example 3-8 shows the code to add to Plane.
Example 3-8. A modifiable speed property
public double SpeedInMilesPerHour
{
get;
set;
}
If we were to review this design with the customer, they might point out that while they
have some systems that do indeed want the speed in miles per hour the people they
liaise with in European air traffic control want the speed in kilometers per hour. To
avoid confusion, we will add another property so that they can get or set the speed in
the units with which they are familiar. Example 3-9 shows a suitable property.
Example 3-9. Property with code in its get and set
public double SpeedInKilometersPerHour
{
get
{
return SpeedInMilesPerHour * 1.609344;
}
set
{
SpeedInMilesPerHour = value / 1.609344;
}
}
We’ve done something different here—rather than just writing get; and set; we’ve
provided code for these accessors. This is another reason we have to declare the accessors explicitly—the C# compiler needs to know whether we want to write a custom
property implementation.
We don’t want to use an ordinary property in Example 3-9, because our SpeedInKilo
metersPerHour is not really a property in its own right—it’s an alternative representation
for the information stored in the SpeedInMilesPerHour property. If we used the normal
property syntax for both, it would be possible to set the speed as being both 100 mph
and 400 km/h, which would clearly be inconsistent. So instead we’ve chosen to implement SpeedInKilometersPerHour as a wrapper around the SpeedInMilesPerHour
property.
If you look at the getter, you’ll see that it returns a value of type double. It is equivalent
to a function with this signature:
public double get_SpeedInKilometersPerHour()
70 | Chapter 3: Abstracting Ideas with Classes and Structs