Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 140 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
140
Dung lượng
2,51 MB
Nội dung
value for each instance of a class. This means that, unlike a const field, if you want a readonly field to be static, you have to declare it as such. Suppose we have an MDI program that edits documents, but that for licensing reasons we want to restrict the number of documents that can be opened simultaneously. Now assume that we are selling different versions of the software, and it’s possible that customers can upgrade their licenses to open more documents simultaneously. Clearly this means we can’t hard-code the maximum number in the source code. We’d probably need a field to represent this maximum number. This field will have to be read in—perhaps from a registry key or some other file storage—each time the program is launched. So our code might look something like this: public class DocumentEditor { public static readonly uint MaxDocuments; static DocumentEditor() { MaxDocuments = DoSomethingToFindOutMaxNumber(); } In this case, the field is static, since the maximum number of documents only needs to be stored once per running instance of the program. This is why it is initialized in the static constructor. If we had an instance readonly field then we would initialize it in the instance constructor(s). For example, presum- ably each document we edit has a creation date, which you wouldn’t want to allow the user to change (because that would be rewriting the past!). Note that the field is also public—we don’t normally need to make readonly fields private, because by definition they cannot be modified externally (the same principle also applies to constants). As noted earlier, date is represented by the class System.DateTime. In the following code we use a System.DateTime constructor that takes three parameters (the year, month, and day of the month— you can find details of this and other DateTime constructors in the MSDN documentation): public class Document { public readonly DateTime CreationDate; public Document() { // read in creation date from file. Assume result is 1 Jan 2002 // but in general this can be different for different instances // of the class CreationDate = new DateTime(2002, 1, 1); } } CreationDate and MaxDocuments in the previous code snippet are treated like any other field, except that because they are read-only, it cannot be assigned to outside the constructors. void SomeMethod() { MaxDocuments = 10; // compilation error here. MaxDocuments is readonly } 100 Chapter 3 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 100 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com It’s also worth noting that you don’t have to assign a value to a readonly field in a constructor. If you don’t do so, it will be left with the default value for its particular data type or whatever value you initial- ized it to at its declaration. That applies to both static and instance readonly fields. Structs So far, we have seen how classes offer a great way of encapsulating objects in your program. We have also seen how they are stored on the heap in a way that gives you much more flexibility in data lifetime, but with a slight cost in performance. This performance cost is small thanks to the optimizations of man- aged heaps. However, there are some situations when all you really need is a small data structure. In this case, a class provides more functionality than you need, and for performance reasons you will probably prefer to use a struct. Look at this example: class Dimensions { public double Length; public double Width; } We’ve defined a class called Dimensions, which simply stores the length and width of some item. Perhaps we’re writing a furniture-arranging program to let people experiment with rearranging their furniture on the computer and we want to store the dimensions of each item of furniture. It looks like we’re breaking the rules of good program design by making the fields public, but the point is that we don’t really need all the facilities of a class for this at all. All we have is two numbers, which we find convenient to treat as a pair rather than individually. There is no need for lots of methods, or for us to be able to inherit from the class, and we certainly don’t want to have the .NET runtime go to the trouble of bringing in the heap with all the performance implications, just to store two doubles. As mentioned earlier in this chapter, the only thing we need to change in the code to define a type as a struct instead of a class is to replace the keyword class with struct: struct Dimensions { public double Length; public double Width; } Defining functions for structs is also exactly the same as defining them for classes. The following code demonstrates a constructor and a property for a struct: struct Dimensions { public double Length; public double Width; Dimensions(double length, double width) { Length=length; Width=width; } public int Diagonal 101 Objects and Types 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 101 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com { { get { return Math.Sqrt(Length*Length + Width*Width); } } } } In many ways you can think of structs in C# as being like scaled-down classes. They are basically the same as classes, but designed more for cases where you simply want to group some data together. They differ from classes in the following ways: ❑ Structs are value types, not reference types. This means they are stored either in the stack or inline (if they are part of another object that is stored on the heap) and have the same lifetime restrictions as the simple data types. ❑ Structs do not support inheritance. ❑ There are some differences in the way constructors work for structs. In particular, the compiler always supplies a default no-parameter constructor, which you are not permitted to replace. ❑ With a struct, you can specify how the fields are to be laid out in memory (we will examine this in Chapter 10 when we cover attributes). Because structs are really intended to group data items together, you’ll sometimes find that most or all of their fields are declared as public. This is strictly speaking contrary to the guidelines for writing .NET code—according to Microsoft, fields (other than const fields) should always be private and wrapped by public properties. However, for simple structs, many developers would nevertheless consider public fields to be acceptable programming practice. C++ developers beware; structs in C# are very different from classes in their implementation. This is very different to the situation in C++, for which classes and structs are virtually the same thing. Let’s look at some of these differences in more detail. Structs Are Value Types Although structs are value types, you can often treat them syntactically in the same way as classes. For example, with our definition of the Dimensions class in the previous section, we could write: Dimensions point = new Dimensions(); point.Length = 3; point.Width = 6; Note that because structs are value types, the new operator does not work in the same way as it does for classes and other reference types. Instead of allocating memory on the heap, the new operator simply calls the appropriate constructor, according to the parameters passed to it, initializing all fields. Indeed, for structs it is perfectly legal to write: 102 Chapter 3 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 102 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Dimensions point; point.Length = 3; point.Width = 6; If Dimensions was a class, this would produce a compilation error, because point would contain an uninitialized reference—an address that points nowhere, so we could not start setting values to its fields. For a struct however, the variable declaration actually allocates space on the stack for the entire struct, so it’s ready to assign values to. Note, however, that the following code would cause a compilation error, with the compiler complaining that you are using an uninitialized variable: Dimensions point; Double D = point.Length; Structs follow the same rules as any other data type: everything must be initialized before use. A struct is considered fully initialized either when the new operator has been called against it, or when values have been individually assigned to all its fields. And of course, a struct defined as a member field of a class is initialized by being zeroed-out automatically when the containing object is initialized. The fact that structs are value types will affect performance, though depending on how you use your struct, this can be good or bad. On the positive side, allocating memory for structs is very fast because this takes place inline or on the stack. The same goes for removing structs when they go out of scope. On the other hand, whenever you pass a struct as a parameter or assign a struct to another struct (as in A=B, where A and B are structs), the full contents of the struct are copied, whereas for a class only the ref- erence is copied. This will result in a performance loss that depends on the size of the struct—this should emphasize the fact that structs are really intended for small data structures. Note, however, that when passing a struct as a parameter to a method, you can avoid this performance loss by passing it as a ref parameter—in this case only the address in memory of the struct will be passed in, which is just as fast as passing in a class. On the other hand, if you do this, you’ll have to be aware that it means the called method can in principle change the value of the struct. Structs and Inheritance Structs are not designed for inheritance. This means that it is not possible to inherit from a struct. The only exception to this is that structs, in common with every other type in C#, derive ultimately from the class System.Object. Hence, structs also have access to the methods of System.Object, and it is even possi- ble to override them in structs—an obvious example would be overriding the ToString() method. The actual inheritance chain for structs is that each struct derives from a class, System.ValueType, which in turn derives from System.Object. ValueType does not add any new members to Object, but provides implementations of some of them that are more suitable for structs. Note that you cannot supply a differ- ent base class for a struct: Every struct is derived from ValueType. Constructors for Structs You can define constructors for structs in exactly the same way that you can for classes, except that you are not permitted to define a constructor that takes no parameters. This may seem nonsensical, and the reason is buried in the implementation of the .NET runtime. There are some rare circumstances in which the .NET runtime would not be able to call a custom zero-parameter constructor that you have supplied. Microsoft has therefore taken the easy way out and banned zero-parameter constructors for structs in C#. 103 Objects and Types 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 103 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com That said, the default constructor, which initializes all fields to zero values, is always present implicitly, even if you supply other constructors that take parameters. It’s also impossible to circumvent the default constructor by supplying initial values for fields. The following code will cause a compile-time error: struct Dimensions { public double Length = 1; // error. Initial values not allowed public double Width = 2; // error. Initial values not allowed Of course, if Dimensions had been declared as a class, this code would have compiled without any problems. Incidentally, you can supply a Close() or Dispose() method for a struct in the same way you do for a class. The Object Class We indicated earlier that all .NET classes are ultimately derived from System.Object. In fact, if you don’t specify a base class when you define a class, the compiler will automatically assume that it derives from Object. Since we have not used inheritance in this chapter, that means that every class we have shown here is actually derived from System.Object. (As noted earlier, for structs this derivation is indi- rect: A struct is always derived from System.ValueType, which in turn derives from System.Object.) The practical significance of this is that, besides the methods and properties and so on that you define, you also have access to a number of public and protected member methods that have been defined for the Object class. These methods are available in all other classes that you define. System.Object Methods The methods defined in Object are as shown in the following table. Method Access Modifiers Purpose string ToString() public virtual Returns a string representation of the object int GetHashTable() public virtual Used if implementing dictionaries (hash tables) bool Equals(object obj) public virtual Compares instances of the object for equality bool Equals(object objA, public static Compares instances of the object for object objB) equality bool ReferenceEquals public static Compares whether two references refer to (object objA, object objB) the same object Type GetType() Public Returns details of the type of the object. object MemberwiseClone() Protected Makes a shallow copy of the object void Finalize() protected virtual This is the .NET version of a destructor 104 Chapter 3 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 104 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com We haven’t yet covered enough of the C# language to be able to understand how to use all these meth- ods. For the time being, we will simply summarize the purpose of each method, with the exception of ToString(), which we examine in more detail. ❑ ToString()—This is intended as a fairly basic, quick-and-easy string representation; use it when you just want a quick idea of the contents of an object, perhaps for debugging purposes. It provides very little choice of how to format the data: For example, dates can in principle be expressed in a huge variety of different formats, but DateTime.ToString() does not offer you any choice in this regard. If you need a more sophisticated string representation that, for exam- ple, takes account of your formatting preferences or of the culture (the locale), then you should implement the IFormattable interface (see Chapter 8). ❑ GetHashCode()—This is used if objects are placed in a data structure known as a map (also known as a hash table or dictionary). It is used by classes that manipulate these structures in order to determine where to place an object in the structure. If you intend your class to be used as key for a dictionary, then you will need to override GetHashCode(). There are some fairly strict requirements for how you implement your overload, and we deal with those when we examine dictionaries in Chapter 9. ❑ Equals() (both versions) and ReferenceEquals()—As you’ll gather by the existence of three different methods aimed at comparing the equality of objects, The .NET Framework has quite a sophisticated scheme for measuring equality. There are subtle differences between how these three methods, along with the comparison operator, ==, are intended to be used. Not only that but there are also restrictions on how you should override the virtual, one parameter version of Equals() if you choose to do so, because certain base classes in the System.Collections namespace call the method and expect it to behave in certain ways. We explore the use of these methods in Chapter 5 when we examine operators. ❑ Finalize()—We cover this method in Chapter 7. It is intended as the nearest that C# has to C++- style destructors, and is called when a reference object is garbage collected to clean up resources. The Object implementation of Finalize() actually does nothing and is ignored by the garbage collector. You will normally override Finalize() if an object owns references to unmanaged resources which need to be removed when the object is deleted. The garbage collector cannot do this directly as it only knows about managed resources, so it relies on any finalizers that you supply. ❑ GetType()—This method returns an instance of a class derived from System.Type. This object can provide an extensive range of information about the class of which your object is a member, including base type, methods, properties, and so on. System.Type also provides the entry point into .NET’s reflection technology. We will examine this topic in Chapter 10. ❑ MemberwiseClone()—This is the only member of System.Object that we don’t examine in detail anywhere in the book. There is no need to, since it is fairly simple in concept. It simply makes a copy of the object and returns a reference (or in the case of a value type, a boxed refer- ence) to the copy. Note that the copy made is a shallow copy—this means that it copies all the value types in the class. If the class contains any embedded references, then only the references will be copied, not the objects referred to. This method is protected and so cannot be called to copy external objects. It is also not virtual, so you cannot override its implementation. The ToString() Method We have already encountered ToString() in Chapter 2. It provides the most convenient way to get a quick string representation of an object. 105 Objects and Types 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 105 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com For example: int i = -50; string str = i.ToString(); // returns “-50” Here’s another example: enum Colors {Red, Orange, Yellow}; // later on in code Colors favoriteColor = Colors.Orange; string str = favoriteColor.ToString(); // returns “Orange” Object.ToString() is actually declared as virtual, and in all these examples, we are taking advantage of the fact that its implementation in the C# predefined data types has been overridden for us in order to return correct string representations of those types. You might not think that our Colors enum counts as a predefined data type. It actually gets implemented as a struct derived from System.Enum, and System.Enum has a rather clever override of ToString() that deals with all the enums you define. If you don’t override ToString() in classes that you define, then your classes will simply inherit the System.Object implementation—which displays the name of the class. If you want ToString() to return a string that contains information about the value of objects of your class, then you will need to override it. We illustrate this with a sample, Money, which defines a very simple class, also called Money, which represent U.S. currency amounts. Money simply acts as a wrapper for the decimal class but sup- plies a ToString() method. Note that this method must be declared as override because it is replac- ing (overriding) the ToString() method supplied by Object. We discuss overriding in more detail in Chapter 4. The complete code for the sample is as follows. Note that it also illustrates use of properties to wrap fields: using System; namespace Wrox.ProCSharp.OOCSharp { class MainEntryPoint { static void Main(string[] args) { Money cash1 = new Money(); cash1.Amount = 40M; Console.WriteLine(“cash1.ToString() returns: “ + cash1.ToString()); Console.ReadLine(); } } class Money { private decimal amount; public decimal Amount { get { return amount; } 106 Chapter 3 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 106 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com set { amount = value; } } public override string ToString() { return “$” + Amount.ToString(); } } } You’ll realize that this sample is there just to illustrate syntactical features of C#. C# already has a prede- fined type to represent currency amounts, decimal, so in real life, you wouldn’t write a class to duplicate this functionality unless you wanted to add various other methods to it. And in many cases due to for- matting requirements, you’d probably use the String.Format() method (which we cover in Chapter 8) rather than ToString() to display a currency string. In the Main() method we instantiate first a Money object, then a BetterMoney object. In both cases we call ToString(). For the Money object, we’ll pick up the Object version of this method that displays class information. For the BetterMoney object, we’ll pick up our own override. Running this code gives the following results: StringRepresentations cash1.ToString() returns: $40 Summary In this chapter we’ve examined C# syntax for declaring and manipulating objects. We have seen how to declare static and instance fields, properties, methods, and constructors. We have also seen that C# adds some new features not present in the OOP model of some other languages: Static constructors provide a means of initializing static fields, while structs allow you to define high-performance types, albeit with a more restricted feature set, which do not require the use of the managed heap. We have also seen how all types in C# derive ultimately from the type System.Object, which means that all types start with a basic set of useful methods, including ToString(). In Chapter 4 we examine implementation and interface inheritance in C#. 107 Objects and Types 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 107 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 05 557599 Ch03.qxd 4/29/04 11:32 AM Page 108 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Inheritance In Chapter 3, we examined how to use individual classes in C#. The focus in that chapter was on how to define methods, constructors, properties, and other members of a single class (or a single struct). Although we did point out the fact that all classes ultimately derive from the class System.Object, we did not examine how to create a hierarchy of inherited classes. Inheritance is the subject of this chapter. We will briefly discuss the scope of C#’s support for inheritance, before examining in detail how to code first implementation inheritance then interface inheritance in C#. Note that this chapter presumes familiarity with the basic concepts of inheritance, including vir- tual functions and overriding. We will concentrate on the syntax used to provide inheritance and inheritance-related topics, such as virtual functions, and on those aspects of the C# inheritance model that are particular to C# and not necessarily shared by other object-oriented languages. Types of Inheritance We’re going to start off by reviewing exactly what C# does and does not support as far as inheri- tance is concerned. Implementation Versus Interface Inheritance Gurus of object-oriented programming will know that there are two distinct types of inheritance: implementation inheritance and interface inheritance. ❑ Implementation inheritance means that a type derives from a base type, taking all the base type’s member fields and functions. With implementation inheritance, a derived type adopts the base type’s implementation of each function, unless it is indicated in the defini- tion of the derived type that a function implementation is to be overridden. This type of inheritance is most useful when you need to add functionality to an existing type, or where a number of related types share a significant amount of common functionality. A good example of this comes in the Windows Forms classes, which we discuss in Chapter 06 557599 Ch04.qxd 4/29/04 11:30 AM Page 109 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... examine C# s implementation of interfaces in detail in this section 123 Chapter 4 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Developers familiar with COM should be aware that, although conceptually C# interfaces are similar to COM interfaces, they are not the same thing The underlying architecture is different For example, C# interfaces do not derive from IUnknown A C# interface... goals behind the development of C# Accordingly, C# does not support multiple implementation inheritance On the other hand, it does allow types to derive from multiple interfaces This means that a C# class can derive from one other class, and any number of interfaces Indeed, we can be more precise: Thanks to the presence of System.Object as a common base type, every C# class (except for Object) has... after division, so for example x % 5 returns 2 if x is equal to 7 1 32 Operators and Casts Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com You will use few pointers in C#, and so, you will use few indirection operators -> Specifically, the only place you will use them is within blocks of unsafe code, because that’s the only place in C# where pointers are allowed Operator Shortcuts... declared as such abstract class Building { public abstract decimal CalculateHeatingCost(); } // abstract method C++ developers will notice some syntactical differences in C# here C# does not support the =0 syntax to declare abstract functions In C#, this syntax would be misleading, since = is allowed in member fields in class declarations to supply initial values: abstract class Building { private bool... class and interfaces is separated by commas: public class MyDerivedClass : MyBaseClass, IInterface1, IInterface2 { // etc For a struct, the syntax is as follows: public struct MyDerivedStruct : IInterface1, IInterface2 { // etc If you do not specify a base class in a class definition, the C# compiler will assume that System.Object is the base class Hence the following two pieces of code yield the same... examine C# s support for operators, operator overloads, and casting between types 130 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Operators and Casts In the preceding chapters, we have covered most of what you need to start writing useful programs using C# In this chapter, we complete our discussion of the essential language elements and go on to discuss powerful aspects of C#. .. data type conversions Operators Although most of C# s operators should be familiar to C and C++ developers, we will discuss the most important ones here for the benefit of new programmers and Visual Basic converts, and to shed light on some of the changes introduced with C# Chapter 5 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com C# supports the operators listed in the following... One of the biggest pitfalls to watch out for when using C# operators is that, like other C-style languages, C# uses different operators for assignment =, and comparison == For instance, the following statement means let x equal three: x = 3; If we now want to compare x to a value, we need to use the double equals sign ==: if (x == 3) Fortunately, C# s strict type safety rules prevent the very common... legal C#, but since your MyGroovyMethod() is not intended to be related in any way to the base class MyGroovyMethod() the result of running this code does not yield the result you want Fortunately C# has been designed in such a way that it copes very well when conflicts of this type arise 113 Chapter 4 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com In these situations, C# generates... types) with these visibilities, since in this case the type also has the status of a member Hence the following code is correct: public class OuterClass { protected class InnerClass { // etc } // etc } 122 Inheritance Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com If you have a nested type, the inner type is always able to see all members of the outer type Hence with the above . inheritance in C#. 107 Objects and Types 05 557599 Ch03.qxd 4 /29 /04 11: 32 AM Page 107 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 05 557599 Ch03.qxd 4 /29 /04 11: 32 AM Page. initializing all fields. Indeed, for structs it is perfectly legal to write: 1 02 Chapter 3 05 557599 Ch03.qxd 4 /29 /04 11: 32 AM Page 1 02 Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com Dimensions. date from file. Assume result is 1 Jan 20 02 // but in general this can be different for different instances // of the class CreationDate = new DateTime (20 02, 1, 1); } } CreationDate and MaxDocuments