Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 30 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
30
Dung lượng
250,72 KB
Nội dung
Object-OrientedProgramming O bject-oriented programming is the third major programming paradigm. At its heart, object-orientedprogramming has a few simple ideas, some of which you’ve already encoun- tered. Possibly the most important idea is that the implementations and state should be encapsulated, that is, hidden behind well-defined boundaries. This makes the structure of a program easier to manage. In F#, things are hidden by using signatures for modules and type definitions and also by simply defining them locally to an expression or class construction (you’ll see examples of both in this chapter). The second idea is that you can implement abstract entities in multiple ways. In OOP this is known as polymorphism. You’ve met a number of simple abstract entities already, such as function types. A function type is abstract because a function with specific type can be imple- mented in many different ways; for example, the function type int -> int can be implemented as a function that increments the given parameter, a function that decrements the parameter, or any one of millions of mathematical sequences. Other abstract entities can be built out of existing abstract components such as the interface types defined in the .NET BCL. More sophisticated abstract entities are modeled using user-defined interface types. Interface types have the advantage that they can be arranged hierarchically; this is called interface inheritance. For example, the .NET BCL includes a hierarchical classification of collection types, available in the System.Collections and System.Collections.Generic namespaces. In OOP you can sometimes arrange implementation fragments hierarchically. This is called implementation inheritance. This tends to be less important in F# programming because of the flexibility that functional programming provides for defining and sharing implementation fragments. However, it is significant for domains such as graphical user inter face (GUI) pr ogr amming. Casting Casting is a way of explicitly altering the static type of a value by either throwing information away, upcasting, or rediscovering it, downcasting. In F#, upcasts and downcasts have their own operators. The type hierarchy starts with obj (or System.Object) at the top and all its descendants below it. An upcast moves a type up the hierarchy, and a downcast moves a type down the hierarchy. Upcasts change a value’s static type to one of its ancestor types. This is a safe operation since the compiler can always tell whether this will work because the compiler always knows all the ancestors of a type so is able to work out through static analysis whether an upcast will 81 CHAPTER 5 ■ ■ ■ 7575Ch05.qxp 4/27/07 1:02 PM Page 81 be successful. An upcast is represented by a colon followed by the greater-than sign (:>). The following code shows an example of using an upcast to convert a string to an obj: #light let myObject = ("This is a string" :> obj) Generally, upcasts are required when defining collections that contain disparate types. If an upcast is not used, the compiler will infer that the collection has the type of the first ele- ment and give a compile error if elements of other types are placed in the collection. The next example demonstrates how to create an array of controls, a pretty common task when work- ing with WinForms. Notice that all the individual controls are upcast to their common base class, Control. #light open System.Windows.Forms let myControls = [| (new Button() :> Control); (new TextBox() :> Control); (new Label() :> Control) |] An upcast also has the effect of automatically boxing any value type. Value types are held in memory on the program stack, rather than on the managed heap. Boxing means that the value is pushed onto the managed heap, so it can be passed around by reference. The follow- ing example demonstrates a value being boxed: #light let boxedInt = (1 :> obj) A downcast changes a value’s static type to one of its descendant types and thus recovers information hidden by an upcast. Downcasting is dangerous since the compiler doesn’t have any way to statically determine whether an instance of a type is compatible with one of its derived types. This means you can get it wrong, and this will cause an invalid cast exception ( System.InvalidCastException) to be issued at runtime. Because of the inherent danger of downcasting, it is often preferred to replace it with pattern matching over .NET types, as demonstrated in Chapter 3. Nevertheless, a downcast can be useful in some places, so a downcast operator, composed of a colon, question mark, and greater-than sign ( :?>), is avail- able. The next example demonstrates downcasting: #light open System.Windows.Forms let moreControls = [| (new Button() :> Control); (new TextBox() :> Control) |] let control = let temp = moreControls.[0] temp.Text <- "Click Me!" temp CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 82 7575Ch05.qxp 4/27/07 1:02 PM Page 82 let button = let temp = (control :?> Button) temp.DoubleClick.Add(fun e -> MessageBox.Show("Hello") |> ignore) temp I t creates an array of two Windows control objects, upcasting them to their base class, Control. Then it binds the first control to the control identifier. It then downcasts this to its specific type, Button, before adding a handler to its DoubleClick event, an event not available on the Control class. Type Tests Closely related to casting is the idea of type tests. An identifier can be bound to an object of a derived type, as you did earlier when you bound a string to an identifier of type obj: #light let anotherObject = ("This is a string" :> obj) Since an identifier can be bound to an object of a derived type, it is often useful to be able to test what this type is. To do this, F# provides a type test operator, which consists of a colon followed by a question mark ( :?). To compile, the operator and its operands must be sur- rounded by parentheses. If the identifier in the type test is of the specified type or a type derived from it, the operator will return true; otherwise, it will return false. The next example shows two type tests, one that will return true and the other false: #light let anotherObject = ("This is a string" :> obj) if (anotherObject :? string) then print_endline "This object is a string" else print_endline "This object is not a string" if (anotherObject :? string[]) then print_endline "This object is a string array" else print_endline "This object is not a string array" F irst y ou cr eate an identifier , anotherObject, of type obj but bind it to a string. Then y ou test whether the anotherObject is a string, which will return true. Then you test whether it is a string array, which will, of course, return false. Type Annotations for Subtyping As shown in Chapter 3, type annotations are a way of constraining an identifier, usually a parameter of a function, to be a certain type. What may seem counterintuitive to an OO pro- grammer is that the form of type annotation introduced in Chapter 3 is rigid; in other words, it does not take into account the inheritance hierarchy. This means that if such a type annota- tion is applied to an expression, then that expression must have precisely that type statically; a derived type will not fit in its place. To illustrate this point, consider the following example: CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 83 7575Ch05.qxp 4/27/07 1:02 PM Page 83 #light open System.Windows.Forms let showForm (form : Form) = form.Show() // PrintPreviewDialog is defined in the BCL and is // derived directly the Form class let myForm = new PrintPreviewDialog() showForm myForm When you try to compile the previous example, you will receive the following error: Prog.fs(11,10): error: FS0001: This expression has type PrintPreviewDialog but is here used with type Form One way to call a function with a rigid type annotation on a parameter is to use an explicit upcast at the place where the function is called in order to change the type to be the same as the type of the function’s parameter. The following line of code changes the type of myForm to be the same as the type of the parameter of showForm: showForm (myForm :> Form) Although upcasting the argument to showForm is a solution, it’s not a very pretty one, because it means littering client code with upcasts. So, F# provides another type annotation, the derived type annotation, in which the type name is prefixed with a hash sign. This has the effect of constraining an identifier to be of a type or any of its derived types. This means you can rewrite the previous example as shown next to remove the need for explicit upcasts in calling code. I think this is a huge benefit to anyone using the functions you define. #light let showFormRevised (form : #Form) = form.Show() // ThreadExceptionDialog is define in the BCL and is // directly derived type of the Form class let anotherForm = new ThreadExceptionDialog(new Exception()) showFormRevised anotherForm Y ou can use this kind of type annotation to tidy up code that uses a lot of casting. F or example, as shown in the “Casting” section earlier in this chapter, a lot of casting is often needed when cr eating a collection with a common base type , and this can leave code looking a little bulkier than it should. A good way to r emo v e this r epeated casting, as with any com - monly r epeated section of code , is to define a function that does it for y ou: CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 84 7575Ch05.qxp 4/27/07 1:02 PM Page 84 #light let myControls = [| (new Button() :> Control); (new TextBox() :> Control); (new Label() :> Control) |] let uc (c : #Control) = c :> Control let myConciseControls = [| uc (new Button()); uc (new TextBox()); uc (new Label()) |] This example shows two arrays of controls being defined. The first, myControls, explicitly upcasts every control; the second, myConciseControls, delegates this job to a function. Also, given that the bigger the array, the bigger the savings and that it is quite common for these arrays to get quite big when working with WinForms, this is a good technique to adopt. Records As Objects It is possible to use the record types you met in Chapter 3 to simulate object-like behavior. This is because records can have fields that are functions, which you can use to simulate an object’s methods. This technique was first invented before functional programming languages had object-oriented constructs as a way of performing tasks that lent themselves well to object-oriented programming. Some programmers still prefer it, because only the function’s type (or as some prefer, its signature) is given in the record definition, so the implementation can easily be swapped without having to define a derived class as you would in object- oriented programming. I discuss this in greater detail in “Object Expressions” and again in “Inheritance” later in this chapter. Let’s take a look at a simple example of records as objects. The next example defines a type, Shape, that has two members. The first member, reposition, is a function type that moves the shape, and the second member, draw, draws the shape. You use the function makeShape to create a new instance of the shape type. The makeShape function implements the reposition functionality for you; it does this by accepting the initPos parameter, which is then stored in a mutable ref cell, which is updated when the reposition function is called. This means the position of the shape is encapsulated, accessible only through the reposition member . Hiding values in this way is a common technique in F# programming. #light open System.Drawing type Shape = { reposition: Point -> unit; draw : unit -> unit } let makeShape initPos draw = let currPos = ref initPos in { reposition = (fun newPos -> currPos := newPos); draw = (fun () -> draw !currPos); } CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 85 7575Ch05.qxp 4/27/07 1:02 PM Page 85 let circle initPos = makeShape initPos (fun pos -> printfn "Circle, with x = %i and y = %i" pos.X pos.Y) let square initPos = makeShape initPos (fun pos -> printfn "Square, with x = %i and y = %i" pos.X pos.Y) let point (x,y) = new Point(x,y) let shapes = [ circle (point (10,10)); square (point (30,30)) ] let moveShapes() = shapes |> List.iter (fun s -> s.draw()) let main() = moveShapes() shapes |> List.iter (fun s -> s.reposition (point (40,40))) moveShapes() main() Circle, with x = 10 and y = 10 Square, with x = 30 and y = 30 Circle, with x = 40 and y = 40 Square, with x = 40 and y = 40 This example may have seemed trivial, but you can actually go quite a long way with this technique . The next example takes things to their natural conclusion, actually drawing the shapes on a form: #light open System open System.Drawing open System.Windows.Forms CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 86 7575Ch05.qxp 4/27/07 1:02 PM Page 86 type Shape = { reposition: Point -> unit; draw : Graphics -> unit } let movingShape initPos draw = let currPos = ref initPos in { reposition = (fun newPos -> currPos := newPos); draw = (fun g -> draw !currPos g); } let movingCircle initPos diam = movingShape initPos (fun pos g -> g.DrawEllipse(Pens.Blue,pos.X,pos.Y,diam,diam)) let movingSquare initPos size = movingShape initPos (fun pos g -> g.DrawRectangle(Pens.Blue,pos.X,pos.Y,size,size) ) let fixedShape draw = { reposition = (fun newPos -> ()); draw = (fun g -> draw g); } let fixedCircle (pos:Point) (diam:int) = fixedShape (fun g -> g.DrawEllipse(Pens.Blue,pos.X,pos.Y,diam,diam)) let fixedSquare (pos:Point) (size:int) = fixedShape (fun g -> g.DrawRectangle(Pens.Blue,pos.X,pos.Y,size,size)) let point (x,y) = new Point(x,y) let shapes = [ movingCircle (point (10,10)) 20; movingSquare (point (30,30)) 20; fixedCircle (point (20,20)) 20; fixedSquare (point (40,40)) 20; ] let mainForm = let form = new Form() let rand = new Random() form.Paint.Add(fun e -> shapes |> List.iter (fun s -> s.draw e.Graphics) ) CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 87 7575Ch05.qxp 4/27/07 1:02 PM Page 87 form.Click.Add(fun e -> shapes |> List.iter (fun s -> s.reposition(new Point(rand.Next(form.Width), rand.Next(form.Height))); form.Invalidate()) ) form [<STAThread>] do Application.Run(mainForm) Again, you define a Shape record type that has the members reposition and draw. Then you define the functions makeCircle and makeSquare to create different kinds of shapes and use them to define a list of shape records. Finally, you define the form that will hold your records. Here you must do a bit more work than perhaps you would like. Since you don’t use inheritance, the BCL’s System.Winows.Forms.Form doesn’t know anything about your shape “objects,” and you must iterate though the list, explicitly drawing each shape. This is actually quite simple to do and takes just three lines of code where you add an event handler to mainForm’s Paint event: temp.Paint.Add( fun e -> List.iter (fun s -> s.draw e.Graphics) shapes); This example shows how you can quickly create multifunctional records without having to worry about any unwanted features you might also be inheriting. In the next section, you’ll look at how you can represent operations on these objects in a more natural way: by adding members to F# types. F# Types with Members It is possible to add functions to both F#’s record and union types. A function added to a record or union type can be called using dot notation, just like a member of a class from a library not written in F#. This provides a convenient way of working with records with mutable state. It is also useful when it comes to exposing types you define in F# to other .NET languages . (I discuss this in more detail in Chapter 13.) Some programmers from object-oriented backgrounds just prefer to see function calls made on an instance value, and this provides a nice way of doing it for all F# types . The syntax for defining an F# record or union type with members is the same as the syn- tax you learned in Chapter 3, except it includes member definitions that always come at the end, betw een the with and end keywor ds. The definition of the members themselves start with the keyword member, followed by an identifier that represents the parameter of the type the member is being attached to, then a dot, then the function name, and then any other parame- ters the function takes. After this comes an equals sign followed by the function definition, which can be any F# expression. The following example defines a record type, point. It has two fields, left and top, and a member function, Swap. The function Swap is a simple function that swaps the values of left CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 88 7575Ch05.qxp 4/27/07 1:02 PM Page 88 and top. Note how the x parameter, given before the function name swap, is used within the function definition to get access to the record’s other members, its fields: #light type Point = { mutable top : int ; mutable left : int } with member x.Swap() = let temp = x.top x.top <- x.left x.left <- temp end let printAnyNewline x = print_any x print_newline() let main() = printAnyNewline myPoint myPoint.Swap() printAnyNewline myPoint main() The results of this example, when compiled and executed, are as follows: {top = 3; left = 7;} {top = 7; left = 3;} You may have noticed the x parameter in the definition of the function Swap: member x.Swap() = let temp = x.top x.top <- x.left x.left <- temp This is the parameter that represents the object on which the function is being called. When a function is called on a value, as follows: myPoint.Swap() the value it is being called on is passed to the function as an argument. This is logical, when you think about it, because the function needs to be able to access the fields and methods of the value on which it is being called. Some OO languages use a specific keyword for this, such as this or Me, but F# lets you choose the name of this parameter by specifying a name for it after the keywor d member , in this case x. CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 89 7575Ch05.qxp 4/27/07 1:02 PM Page 89 Union types can have member functions too. You define them in the same way as for record t ypes. The next example shows a union type, D rinkAmount , that has a function added to it: #light type DrinkAmount = | Coffee of int | Tea of int | Water of int with override x.ToString() = match x with | Coffee x -> Printf.sprintf "Coffee: %i" x | Tea x -> Printf.sprintf "Tea: %i" x | Water x -> Printf.sprintf "Water: %i" x end let t = Tea 2 print_endline (t.ToString()) The results of this example, when compiled and executed, are as follows: Tea: 2 Note how this uses the keyword override in place of the keyword member. This has the effect of replacing, or overriding, an existing function of the type. This is not that common a practice with function members associated with F# types because only four methods are available to be overridden ( ToString, Equals, GetHashCode, and Finalize) that are inherited from System.Object by every .NET type. Because of the way some of these methods interact with the CLR, the only one I recommend overriding is ToString. Only four methods are avail- able for overriding because record and union types can’t act as base or derived classes, so you cannot inherit methods to override (except from System.Object). Object Expressions Object expressions are at the heart of succinct object-orientedprogramming in F#. They pro- vide a concise syntax to create an object that inherits from an existing type. This is useful if y ou want to pro vide a short implementation of an abstract class or an interface or want to tweak an existing class definition. An object expression allows you to provide an implementa- tion of a class or interface while at the same time creating a new instance of it. The syntax is similar to the alter ative syntax for cr eating new instances of record types, with a few small alterations. You surround the definition of an object expression with braces. At the beginning is the name of the class or interfaces, and the name of a class must be fol- lowed b y a pair of parentheses that can have any values passed to the constructor between them. Interface names need nothing after them, though both class names and interface names can have a type parameter following them, which must be surrounded by angled brackets . This is followed by the keyword with and the definition of the methods of the class or CHAPTER 5 ■ OBJECT-ORIENTEDPROGRAMMING 90 7575Ch05.qxp 4/27/07 1:02 PM Page 90 [...]... CHAPTER 5 I OBJECT-ORIENTED PROGRAMMING #light [] type ChordScale = | C = 0b0000000000000001 | D = 0b0000000000000010 | E = 0b0000000000000100 | F = 0b0000000000001000 | G = 0b0000000000010000 | A = 0b0000000000100000 | B = 0b0000000001000000 The module Enum provides functionality for dealing with enums in F#; I discuss it in Chapter 7 Summary You’ve now seen how to use the three major programming. .. use the comparer by defining an array and then sorting using the comparer and displaying the “before” and “after” results in the console 91 7575Ch05.qxp 92 4/27/07 1:02 PM Page 92 CHAPTER 5 I OBJECT-ORIENTED PROGRAMMING It is possible to implement multiple interfaces or a class and several other interfaces within one object expression It is not possible to implement more than one class within an object... !height) temp.ToArray() let numbersForm = let temp = new Form() in temp.Controls.AddRange(numbers); temp [] do Application.Run(numbersForm) 7575Ch05.qxp 4/27/07 1:02 PM Page 93 CHAPTER 5 I OBJECT-ORIENTED PROGRAMMING The previous example shows the definition of object expression that implements both the class Control and the interface IComparable IComparable allows objects that implement this interface... class Implementation implements the interface MyInterface #light type MyInterface = interface abstract ChangeState : myInt : int -> unit end 93 7575Ch05.qxp 94 4/27/07 1:02 PM Page 94 CHAPTER 5 I OBJECT-ORIENTED PROGRAMMING type Implementation = class val mutable state : int new() = {state = 0} interface MyInterface with member x.ChangeState y = x.state unit end 7575Ch05.qxp 4/27/07 1:02 PM Page 95 CHAPTER 5 I OBJECT-ORIENTED PROGRAMMING type Implementation = class val mutable state : int new() = {state = 0} interface MyInterface with member x.ChangeState y = x.state . Object-Oriented Programming O bject-oriented programming is the third major programming paradigm. At its heart, object-oriented programming. before functional programming languages had object-oriented constructs as a way of performing tasks that lent themselves well to object-oriented programming.