Inheritance

F Sharp Programming · Wikibooks

0%

Contents

    Text

    Size
    100%
    Theme
    Typeface
    Line length
    Spacing

    From Wikibooks, under CC BY-SA 4.0, as the source states it. This page at Wikibooks.

    Inheritance
    Previous: Classes Index Next: Interfaces
    F# : Inheritance

    Many object-oriented languages use inheritance extensively in the .NET BCL to construct class hierarchies.

    Subclasses

    A subclass is, in the simplest terms, a class derived from a class which has already been defined. A subclass inherits its members from a base class in addition to adding its own members. A subclass is defined using the inherit keyword as shown below:

    type Person(name) =
        member x.Name = name        
        member x.Greet() = printfn "Hi, I'm %s" x.Name
        
    type Student(name, studentID : int) =
        inherit Person(name)
        
        let mutable _GPA = 0.0
        
        member x.StudentID = studentID
        member x.GPA
            with get() = _GPA
            and set value = _GPA <- value
        
    type Worker(name, employer : string) = 
        inherit Person(name)
        
        let mutable _salary = 0.0
        
        member x.Salary
            with get() = _salary
            and set value = _salary <- value
        
        member x.Employer = employer
    

    Our simple class hierarchy looks like this:

    System.Object (* All classes descend from  *)
     - Person
       - Student
       - Worker
    

    The Student and Worker subclasses both inherit the Name and Greet methods from the Person base class. This can be demonstrated in fsi:

    > let somePerson, someStudent, someWorker =
        new Person("Juliet"), new Student("Monique", 123456), new Worker("Carla", "Awesome Hair Salon");;
    
    val someWorker : Worker
    val someStudent : Student
    val somePerson : Person
    
    > somePerson.Name, someStudent.Name, someWorker.Name;;
    val it : string * string * string = ("Juliet", "Monique", "Carla")
    
    > someStudent.StudentID;;
    val it : int = 123456
    
    > someWorker.Employer;;
    val it : string = "Awesome Hair Salon"
    
    > someWorker.ToString();; (* ToString method inherited from System.Object *)
    val it : string = "FSI_0002+Worker"
    

    .NET's object model supports single-class inheritance, meaning that a subclass is limited to one base class. In other words, its not possible to create a class which derives from Student and Employee simultaneously.

    Overriding Methods

    Occasionally, you may want a derived class to change the default behavior of methods inherited from the base class. For example, the output of the .ToString() method above isn't very useful. We can override that behavior with a different implementation using the override:

    type Person(name) =
        member x.Name = name        
        member x.Greet() = printfn "Hi, I'm %s" x.Name
        
        override x.ToString() = x.Name    (* The ToString() method is inherited from System.Object *)
    

    We've overridden the default implementation of the ToString() method, causing it to print out a person's name.

    Methods in F# are not overridable by default. If you expect users will want to override methods in a derived class, you have to declare your method as overridable using the abstract and default keywords as follows:

    type Person(name) =
        member x.Name = name        
        
        abstract Greet : unit -> unit
        default x.Greet() = printfn "Hi, I'm %s" x.Name
        
    type Quebecois(name) =
        inherit Person(name)
        
        override x.Greet() = printfn "Bonjour, je m'appelle %s, eh." x.Name
    

    Our class Person provides a Greet method which may be overridden in derived classes. Here's an example of these two classes in fsi:

    > let terrance, phillip = new Person("Terrance"), new Quebecois("Phillip");;
    
    val terrance : Person
    val phillip : Quebecois
    
    > terrance.Greet();;
    Hi, I'm Terrance
    val it : unit = ()
    
    > phillip.Greet();;
    Bonjour, je m'appelle Phillip, eh.
    

    Abstract Classes

    An abstract class is one which provides an incomplete implementation of an object, and requires a programmer to create subclasses of the abstract class to fill in the rest of the implementation. For example, consider the following:

    [<AbstractClass>]
    type Shape(position : Point) =
        member x.Position = position
        override x.ToString() =
            sprintf "position = {%i, %i}, area = %f" position.X position.Y (x.Area())
        
        abstract member Draw : unit -> unit 
        abstract member Area : unit -> float
    

    The first thing you'll notice is the AbstractClass attribute, which tells the compiler that our class has some abstract members. Additionally, you notice two abstract members, Draw and Area don't have an implementation, only a type signature.

    We can't create an instance of Shape because the class hasn't been fully implemented. Instead, we have to derive from Shape and override the Draw and Area methods with a concrete implementation:

    type Circle(position : Point, radius : float) =
        inherit Shape(position)
        
        member x.Radius = radius
        override x.Draw() = printfn "(Circle)"
        override x.Area() = Math.PI * radius * radius
        
    type Rectangle(position : Point, width : float, height : float) =
        inherit Shape(position)
        
        member x.Width = width
        member x.Height = height
        override x.Draw() = printfn "(Rectangle)"
        override x.Area() = width * height
        
    type Square(position : Point, width : float) =
        inherit Shape(position)
        
        member x.Width = width
        member x.ToRectangle() = new Rectangle(position, width, width)
        override x.Draw() = printfn "(Square)"
        override x.Area() = width * width
        
    type Triangle(position : Point, sideA : float, sideB : float, sideC : float) =
        inherit Shape(position)
        
        member x.SideA = sideA
        member x.SideB = sideB
        member x.SideC = sideC
        
        override x.Draw() = printfn "(Triangle)"
        override x.Area() =
            (* Heron's formula *)
            let a, b, c = sideA, sideB, sideC
            let s = (a + b + c) / 2.0
            Math.Sqrt(s * (s - a) * (s - b) * (s - c) )
    

    Now we have several different implementations of the Shape class. We can experiment with these in fsi:

    > let position = { X = 0; Y = 0 };;
    
    val position : Point
    
    > let circle, rectangle, square, triangle =
        new Circle(position, 5.0),
        new Rectangle(position, 2.0, 7.0),
        new Square(position, 10.0),
        new Triangle(position, 3.0, 4.0, 5.0);;
    
    val triangle : Triangle
    val square : Square
    val rectangle : Rectangle
    val circle : Circle
    
    > circle.ToString();;
    val it : string = "Circle, position = {0, 0}, area = 78.539816"
    
    > triangle.ToString();;
    val it : string = "Triangle, position = {0, 0}, area = 6.000000"
    
    > square.Width;;
    val it : float = 10.0
    
    > square.ToRectangle().ToString();;
    val it : string = "Rectangle, position = {0, 0}, area = 100.000000"
    
    > rectangle.Height, rectangle.Width;;
    val it : float * float = (7.0, 2.0)
    

    Working With Subclasses

    Up-casting and Down-casting

    A type cast is an operation which changes the type of an object from one type to another. This is not the same as a map function, because a type cast does not return an instance of a new object, it returns the same instance of an object with a different type.

    For example, let's say B is a subclass of A. If we have an instance of B, we are able to cast as an instance of A. Since A is upward in the class hierarchy, we call this an up-cast. We use the :> operator to perform upcasts:

    > let regularString = "Hello world";;
    
    val regularString : string
    
    > let upcastString = "Hello world" :> obj;;
    
    val upcastString : obj
    
    > regularString.ToString();;
    val it : string = "Hello world"
    
    > regularString.Length;;
    val it : int = 11
    
    > upcastString.ToString();; (* type obj has a .ToString method *)
    val it : string = "Hello world"
    
    > upcastString.Length;; (* however, obj does not have Length method *)
    
      upcastString.Length;; (* however, obj does not have Length method *)
      -------------^^^^^^^
    
    stdin(24,14): error FS0039: The field, constructor or member 'Length' is not defined.
    

    Up-casting is considered "safe", because a derived class is guaranteed to have all of the same members as an ancestor class. We can, if necessary, go in the opposite direction: we can down-cast from an ancestor class to a derived class using the :?> operator:

    > let intAsObj = 20 :> obj;;
    
    val intAsObj : obj
    
    > intAsObj, intAsObj.ToString();;
    val it : obj * string = (20, "20")
    
    > let intDownCast = intAsObj :?> int;;
    
    val intDownCast : int
    
    > intDownCast, intDownCast.ToString();;
    val it : int * string = (20, "20")
    
    > let stringDownCast = intAsObj :?> string;; (* boom! *)
    
    val stringDownCast : string
    
    System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
       at <StartupCode$FSI_0067>.$FSI_0067._main()
    stopped due to error
    

    Since intAsObj holds an int boxed as an obj, we can downcast to an int as needed. However, we cannot downcast to a string because it is an incompatible type. Down-casting is considered "unsafe" because the error isn't detectable by the type-checker, so an error with a down-cast always results in a runtime exception.

    Up-casting example

    open System
    
    type Point = { X : int; Y : int }
    
    [<AbstractClass>]
    type Shape() =
        override x.ToString() =
            sprintf "%s, area = %f" (x.GetType().Name) (x.Area())
        
        abstract member Draw : unit -> unit 
        abstract member Area : unit -> float
        
    type Circle(radius : float) =
        inherit Shape()
        
        member x.Radius = radius
        override x.Draw() = printfn "(Circle)"
        override x.Area() = Math.PI * radius * radius
        
    type Rectangle(width : float, height : float) =
        inherit Shape()
        
        member x.Width = width
        member x.Height = height
        override x.Draw() = printfn "(Rectangle)"
        override x.Area() = width * height
        
    type Square(width : float) =
        inherit Shape()
        
        member x.Width = width
        member x.ToRectangle() = new Rectangle(width, width)
        override x.Draw() = printfn "(Square)"
        override x.Area() = width * width
        
    type Triangle(sideA : float, sideB : float, sideC : float) =
        inherit Shape()
        
        member x.SideA = sideA
        member x.SideB = sideB
        member x.SideC = sideC
        
        override x.Draw() = printfn "(Triangle)"
        override x.Area() =
            (* Heron's formula *)
            let a, b, c = sideA, sideB, sideC
            let s = (a + b + c) / 2.0
            Math.Sqrt(s * (s - a) * (s - b) * (s - c) )
    
    let shapes =
            [(new Circle(5.0) :> Shape);
                (new Circle(12.0) :> Shape);
                (new Square(10.5) :> Shape);
                (new Triangle(3.0, 4.0, 5.0) :> Shape);
                (new Rectangle(5.0, 2.0) :> Shape)]
            (* Notice we have to cast each object as a Shape *)
                
    let main() = 
        shapes
        |> Seq.iter (fun x -> printfn "x.ToString: %s" (x.ToString()) )
    
    main()
    

    This program has the following types:

    type Point =
      {X: int;
       Y: int;}
    
    type Shape =
      class
        abstract member Area : unit -> float
        abstract member Draw : unit -> unit
        new : unit -> Shape
        override ToString : unit -> string
      end
    
    type Circle =
      class
        inherit Shape
        new : radius:float -> Circle
        override Area : unit -> float
        override Draw : unit -> unit
        member Radius : float
      end
    
    type Rectangle =
      class
        inherit Shape
        new : width:float * height:float -> Rectangle
        override Area : unit -> float
        override Draw : unit -> unit
        member Height : float
        member Width : float
      end
    
    type Square =
      class
        inherit Shape
        new : width:float -> Square
        override Area : unit -> float
        override Draw : unit -> unit
        member ToRectangle : unit -> Rectangle
        member Width : float
      end
    
    type Triangle =
      class
        inherit Shape
        new : sideA:float * sideB:float * sideC:float -> Triangle
        override Area : unit -> float
        override Draw : unit -> unit
        member SideA : float
        member SideB : float
        member SideC : float
      end
    
    val shapes : Shape list
    

    This program outputs:

    x.ToString: Circle, area = 78.539816
    x.ToString: Circle, area = 452.389342
    x.ToString: Square, area = 110.250000
    x.ToString: Triangle, area = 6.000000
    x.ToString: Rectangle, area = 10.000000
    

    Public, Private, and Protected Members

    Previous: Classes Index Next: Interfaces