Programming C# 4.0 phần 2 ppt

78 339 0
Programming C# 4.0 phần 2 ppt

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

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 im- portantly, 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 get- ter 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-and- white 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 ex- ample 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 protec- tion 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 low- est 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 de- scribed 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 pa- rameters, 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 construc- tor 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 op- eration, 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 be- cause 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 acces- sors 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 im- plement 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 The setter seems to provide an invisible parameter called value, which is also of type double. So it is equivalent to a method with this signature: public void set_SpeedInKilometersPerHour(double value) This value parameter is a contextual keyword—C# only considers it to be a keyword in property or event accessors. (Events are described in Chapter 5.) This means you’re allowed to use value as an identifier in other contexts—for example, you can write a method that takes a pa- rameter called value. You can’t do that with other keywords—you can’t have a parameter called class, for example. This is a very flexible system indeed. You can provide properties that provide real stor- age in the class to store their data, or calculated properties that use any mechanism you like to get and/or set the values concerned. This choice is an implementation detail hidden from users of our class—we can switch between one and the other without changing our class’s public face. For example, we could switch the implementation of these speed properties around so that we stored the value in kilometers per hour, and calculated the miles per hour—Example 3-10 shows how these two properties would look if the “real” value was in km/h. Example 3-10. Swapping over the real and calculated properties public double SpeedInMilesPerHour { get { return SpeedInKilometersPerHour / 1.609344; } set { SpeedInKilometersPerHour = value * 1.609344; } } public double SpeedInKilometersPerHour { get; set; } As far as users of the Plane class are concerned, there’s no discernible difference between the two approaches—the way in which properties work is an encapsulated implemen- tation detail. Example 3-11 shows an updated Main function that uses the new prop- erties. It neither knows nor cares which one is the “real” one. Defining Classes | 71 Download from Library of Wow! eBook <www.wowebook.com> Example 3-11. Using the speed properties static void Main(string[] args) { Plane someBoeing777 = new Plane("BA0049"); someBoeing777.SpeedInMilesPerHour = 150.0; Console.WriteLine( "Your plane has identifier {0}, " + "and is traveling at {1:0.00}mph [{2:0.00}kph]", someBoeing777.Identifier, someBoeing777.SpeedInMilesPerHour, someBoeing777.SpeedInKilometersPerHour); someBoeing777.SpeedInKilometersPerHour = 140.0; Console.WriteLine( "Your plane has identifier {0}, " + "and is traveling at {1:0.00}mph [{2:0.00}kph]", someBoeing777.Identifier, someBoeing777.SpeedInMilesPerHour, someBoeing777.SpeedInKilometersPerHour); Console.ReadKey(); } Although our public API supports two different units for speed while successfully keeping the implementation for that private, there’s something unsatisfactory about that implementation. Our conversion relies on a magic number (1.609344) that appears repeatedly. Repetition impedes readability, and is prone to typos (I know that for a fact. I’ve typed it incorrectly once already this morning while preparing the example!) There’s an important principle in programming: don’t repeat yourself (or dry, as it’s sometimes abbreviated). Your code should aim to express any single fact or concept no more than once, because that way, you only need to get it right once. It would be much better to put this conversion factor in one place, give it a name, and refer to it by that instead. We can do that by declaring a field. Fields: A Place to Put Data A field is a place to put some data of a particular type. There’s no option to add code like you can in a property—a field is nothing more than data. Back before C# 3.0 the compiler didn’t let us write just get; and set;—we always had to write properties with code as in Example 3-9, and if we wanted a simple property that stored a value, we had to provide a field, with code such as Example 3-12. 72 | Chapter 3: Abstracting Ideas with Classes and Structs Example 3-12. Writing your own simple property // Field to hold the SpeedInMilesPerHour property's value double speedInMilesPerHourValue; public double SpeedInMilesPerHour { get { return speedInMilesPerHourValue; } set { speedInMilesPerHourValue = value; } } When you write just get; and set; as we did in Example 3-8, the C# compiler generates code that’s more or less identical to Example 3-12, except it gives the field a peculiar name to prevent us from accessing it directly. (These compiler-generated properties are called auto properties.) So, if we want to store a value in an object, there’s always a field involved, even if it’s a hidden one provided automatically by the compiler. Fields are the only class members that can hold information—properties are really just methods in disguise. As you can see, a field declaration looks similar to the start of a property declaration. There’s the type (double), and a name. By convention, this name is camelCased, to make fields visibly different from properties. (Some developers like to distinguish fields further by giving them a name that starts with an underscore.) We can modify a field’s protection level if we want, but, conventionally, we leave all fields with the default private accessibility. That’s because a field is just a place for some data, and if we make it public, we lose control over the internal state of our object. Properties always involve some code, even if it’s generated automatically by the com- piler. We can use private backing fields as we wish, or calculate property values any way we like, and we’re free to modify the implementation without ever changing the public face of the class. But with a field, we have nowhere to put code, so if we decide to change our implementation by switching from a field to a calculated value, we would need to remove the field entirely. If the field was part of the public contract of the class, that could break our clients. In short, fields have no innate capacity for encapsulation, so it’s a bad idea to make them public. Example 3-13 shows a modified version of the Plane class. Instead of repeating the magic number for our speed conversion factor, we declare a single field initialized to the required value. Not only does this mean that we get to state the conversion value just once, but we’ve also been able to give it a descriptive name—in the conversions, it’s now obvious that we’re multiplying and dividing by the number of kilometers in a mile, even if you happen not to have committed the conversion factor to memory. Defining Classes | 73 [...]... using the enumeration instead of an integer Example 3 -22 shows the property to add to the Plane class Example 3 -22 Property with enum type public DirectionOfApproach Direction { get; set; } There are some optional features we can use in an enum declaration Example 3 -23 uses these, and they provide some insight into how enum types work Example 3 -23 Explicit type and values for enum enum DirectionOfApproach... described in Chapter 20 .) If all your constructors take parameters, then even if you provide default values for all the parameters, that’s not good enough You can write, say, new MyControl() in C#, but only because the C# compiler is implicitly passing the missing values for you Not everything in the world of NET understands the concept of default arguments (C# itself didn’t until version 4.0. ) Sometimes... we stored it in a named variable, as Example 3 -24 shows Example 3 -24 Storing a reference in a variable Plane someBoeing777 = new Plane("BA0049"); someBoeing777.Direction = DirectionOfApproach.Approaching; We can define another variable with a different name, and store a reference to the same plane in that new variable, as shown in Example 3 -25 Example 3 -25 Copying a reference from one variable to another... first.Position.Altitude / feetPerMile; second.Position.Altitude / feetPerMile; double dx = x1 - x2; double dy = y1 - y2; double dz = z1 - z2; double distanceSquared = dx * dx + dy * dy + dz * dz; double minimumSquared = minimumMiles * minimumMiles; return distanceSquared < minimumSquared; } private const double feetPerMile = 528 0; We’ve seen plenty of function declarations like this before, but we’ll quickly recap... and set accessors are methods under the covers So C# won’t let us use our properties until the underlying fields have been initialized, and we can’t do that because these are auto properties—the C# compiler has generated hidden fields that we can only access through the properties This is a bit of a chicken-and-egg bootstrapping problem! Fortunately, C# gives us a way of calling one of our constructors... terms of another public void SendMessage(string messageName) { SendMessage(messageName, TimeSpan.Zero); } (TimeSpan.Zero is a static field that returns a duration of zero.) Until C# 4.0 that’s as far as we could go However, the C# designers noticed that a lot of member overloads were just like this one: facades over an über-implementation, with a bunch of parameters defaulted out to particular values... using the names of the parameters, like this: MyMethod( 127 , thirdHere: "New third parameter"); With just one method, we now have many different ways to call it—we can provide all the arguments, or just the first and second, or perhaps the first, second, and third There are many combinations Before named arguments and defaults were added in C# 4.0, the only way to get this kind of flexibility was to... Direction property’s type is int, there’s nothing to stop us from saying something like: someBoeing777.Direction = 72; This makes no sense, but the C# compiler doesn’t know that—after all, we told it the property’s type was int, so how’s it supposed to know that’s wrong? Fortunately, the designers of C# have thought of this, and have given us a kind of type for precisely this situation, called an enum, and... Object Initializers Until C# 3.0, the only real solution to this was to write one or more factory methods These are described in the sidebar below But now we have another option Factory Methods A factory method is a static method that builds a new object There’s no formal support for this in C#, it’s just a common solution to a problem—a pattern, as popular idioms are often called in programming We can get... fact that neither of the objects is in any way more significant than the other in this calculation Example 3-41 Detecting when Planes are too close public static { double x1 double x2 double y1 double y2 double z1 double z2 bool TooClose(Plane first, Plane second, double minimumMiles) = = = = = = first.Position.Distance * Math.Cos(first.Position.Angle); second.Position.Distance * Math.Cos(second.Position.Angle); . Toppings { None = 0x 00, // Special zero value ChocolateSauce = 0x01, ToffeeSauce = 0x 02, ChocolateSprinkles = 0x 04 , Chocoholic = 0x05, // Combined value, sets 2 bits Greedy = 0x07 // Everything! } We’re. Plane("BA0 04 9 "); someBoeing777.SpeedInMilesPerHour = 1 50. 0; Console.WriteLine( "Your plane has identifier {0} , " + "and is traveling at {1 :0. 00} mph [ {2 :0. 00} kph]", . someBoeing777.SpeedInKilometersPerHour = 1 40 .0; Console.WriteLine( "Your plane has identifier {0} , " + "and is traveling at {1 :0. 00} mph [ {2 :0. 00} kph]", someBoeing777.Identifier,

Ngày đăng: 06/08/2014, 09:20

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan