# 五、面向对象编程

面向对象编程是第三种主要的编程范式。有一种倾向是试图将功能范式和面向对象范式显示为相互竞争的，但我相信它们是互补的技术，可以很好地协同工作，我将在本章中尝试演示这一点。在本质上，面向对象编程有一些简单的思想，有时被称为面向对象编程的原则:封装、多态和继承。

可能最重要的原则是*封装*—实现和状态应该是*封装*的想法，或者隐藏在定义良好的边界之后。这使得程序的结构更容易管理。在 F# 中，通过使用模块和类型定义的签名，以及通过简单地将它们本地定义到表达式或类构造中，您可以隐藏一些东西(您将在本章中看到这两者的示例)。

第二个原则，*多态性*，是您可以用多种方式实现抽象实体的想法。您已经遇到了许多简单的抽象实体，例如函数类型。函数类型是抽象的，因为您可以用许多不同的方式实现具有特定类型的函数。例如，您可以将函数类型`int -> int`实现为给定参数递增的函数、参数递减的函数或数百万个数学序列中的任何一个。您还可以从现有的抽象组件中构建其他抽象实体，例如。NET 密件抄送。您还可以使用用户定义的接口类型来建模更复杂的抽象实体。接口类型的优点是可以分层排列；这叫*接口继承*。例如。NET BCL 包括集合类型的分层分类，在`System.Collections`和`System.Collections.Generic`名称空间中可用。

在 OOP 中，您有时可以分层排列实现片段。这被称为*实现继承*，在 F# 编程中它往往不那么重要，因为函数式编程为定义和共享实现片段提供了灵活性。然而，它对于诸如图形用户界面(GUI)编程之类的领域是有意义的。

虽然面向对象编程的原则很重要，但面向对象编程也已经成为围绕系统的值组织代码的代名词*名词*，然后提供对这些值的操作，作为对该值进行操作的成员、函数或方法。这通常就像取一个以函数应用于某个值的样式编写的函数(如`String.length s`)并用点符号重写它(如`s.Length`)一样简单。这个简单的动作通常可以让你的代码更加清晰。在本章中，您将看到 F# 如何允许您将成员附加到它的任何类型，而不仅仅是它的类，如果您愿意的话，使您能够以面向对象的方式组织您的所有代码。

F# 提供了丰富的面向对象编程模型，允许您创建行为类似于 C#和 VB.NET 创建的类、接口和对象。也许更重要的是，当打包到库中并由该库的用户查看时，您在 F# 中创建的类与用其他语言创建的类是无法区分的。然而，面向对象编程不仅仅是简单地定义对象，当您开始研究如何使用 F# 本机类型以面向对象的方式编程时，您将会看到这一点。

## 带成员的 F# 类型

可以向 F# 的记录和联合类型添加函数。您可以使用点标记法调用添加到记录或联合类型的函数，就像您可以从非 F# 编写的库中调用类的成员一样。当您想要向其他人公开您在 F# 中定义的类型时，它也证明是有用的。NET 语言。许多程序员更喜欢看到对实例值的函数调用，这种技术为所有 F# 类型提供了一种很好的方法。

定义带有成员的 F# 记录或联合类型的语法与您在[第 4 章](04.html#_Chapter_4_)中学习的语法相同，除了这里它包括总是出现在最后的成员定义，在`with`关键字之后。成员本身的定义以关键字`member`开始，后面跟着一个标识符，该标识符代表成员被附加到的类型的参数，后面跟着一个点、函数名，然后是函数采用的任何其他参数。在这之后是等号后面是函数定义，可以是任何 F# 表达式。

以下示例定义了记录类型`Point`。它有两个字段，`Left`和`Top`；和成员函数`Swap`。`Swap`函数是一个简单的函数，用交换的`Left`和`Top`的值创建一个新点。注意如何在函数定义中使用在函数名`Swap`之前给出的`x`参数来访问记录的其他成员:

```fs
    // A point type.
    type Point =
        { Top: int;
          Left: int }
        with
            // The swap member creates a new point
            // with the left/top coords reversed.
            member x.Swap() =
                { Top = x.Left;
                  Left = x.Top }

    // Create a new point.
    let myPoint =
        { Top = 3;
          Left = 7 }

    let main() =
        // Print the inital point.
        printfn "%A" myPoint
        // Create a new point with the coordinates swapped.
        let nextPoint = myPoint.Swap()
        // Print the new point.
        printfn "%A" nextPoint

    // Start the app.
    do main()

```

当您编译并执行这个示例时，您会得到以下结果:

{ Top = 3；

左= 7；}

{ Top = 7；

左= 3；}

您可能已经注意到函数`Swap`定义中的`x`参数:

```fs
    member x.Swap() =
        { Top = x.Left;
          Left = x.Top }

```

这是表示正在调用函数的对象的参数。现在看一下对一个值调用函数的情况:

```fs
    let nextPoint = myPoint.Swap()

```

调用函数的值作为参数传递给函数。当您考虑到函数需要能够访问调用它的值的字段和方法时，这是合乎逻辑的。有些 OO 语言对此使用特定的关键字，例如`this`或`Me`，但是在这种情况下，F# 允许您通过在关键字成员— `x`之后指定名称来选择该参数的名称。

联合类型也可以有成员函数。定义它们的方式与定义记录类型的方式相同。下一个示例显示了一个联合类型`DrinkAmount`，其中添加了一个函数:

```fs
    // A type representing the amount of a specific drink.
    type DrinkAmount =
        | Coffee of int
        | Tea of int
        | Water of int
        with
            // Get a string representation of the value.
            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

    // Create a new instance of DrinkAmount.
    let t = Tea 2

    // Print out the string.
    printfn "%s" (t.ToString())

```

当您编译并执行这段代码时，您会得到以下结果:

茶:2

注意这是如何使用关键字`override`代替关键字`member`的。这具有替换或*覆盖*基本类型的现有功能的效果。对于与 F# 类型相关联的函数成员来说，这不是一个很常见的做法，因为只有四种方法可以被覆盖:`ToString`、`Equals`、`GetHashCode`和`Finalize`。每一个。NET 类型从`System.Object`继承了这些。由于这些方法中的一些与 CLR 交互的方式，我唯一建议覆盖的是`ToString`。只有四种方法可用于重写，因为记录和联合类型不能作为基类或派生类，所以您不能继承方法来重写(从`System.Object`开始除外)。

## 定义类别

您已经看到了很多使用。NET 密件库；接下来，您将学习如何定义自己的类。在面向对象编程中，一个类应该对您正在创建的程序或库中使用的一些概念进行建模。例如，`String`类仿真一组字符，`Process`类仿真一个操作系统进程。

类是一种类型，因此类定义以`type`关键字开始，后面是类的名称和类的构造函数的参数(位于括号之间)。接下来是等号，后面是类的成员定义。类中最基本的成员叫做*方法*，这是一个可以访问类参数的函数。

下一个示例显示了一个表示用户的类。user 类的构造函数接受两个参数:用户名和用户密码的散列。你的类提供了两种成员方法:`Authenticate`，检查用户的密码是否有效；和`LogonMessage`，获取用户特定的登录消息:

```fs
    open System.Web.Security
    // Give shorter name to password hashing method.
    let hash = FormsAuthentication.HashPasswordForStoringInConfigFile

    // A class that represents a user.
    // Its constructor takes two parameters: the user's
    // name and a hash of his or her password.
    type User(name, passwordHash) =
        // Hashes the user's password and checks it against
        // the known hash.
        member x.Authenticate(password) =
            let hashResult = hash (password, "sha1")
            passwordHash = hashResult

        // Gets the user's logon message.
        member x.LogonMessage() =
            Printf.sprintf "Hello, %s" name

    // Create a new instance of our user class.
    let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")

    let main() =
        // Authenticate user and print appropriate message.
        if user.Authenticate("badpass") then
            printfn "%s" (user.LogonMessage())
        else
            printfn "Logon failed"

    do main()

```

该示例的后半部分演示了如何使用类。它的行为与您在。NET 密件抄送。您可以使用`new`关键字创建`User`的新实例，然后调用其成员方法。

定义类内部的值通常很有用。也许您需要预先计算一个在几个成员方法之间共享的值，或者也许您需要从外部数据源检索对象的一些数据。为了实现这一点，对象可以具有对象内部的`let`绑定，但是在对象的所有成员之间共享。您可以将`let`绑定放在类定义的开头，等号后面，但在第一个成员定义之前。这些`let`绑定形成一个隐式构造，当对象被构造时执行；如果`let`绑定有任何副作用，这些也会在构建对象时发生。如果需要调用具有`unit`类型的函数，例如记录对象的构造时，必须在函数调用前加上`do`关键字。

下一个例子演示了私有的`let`绑定，方法是获取您的`User`类并对其稍加修改。现在类构造器取一个`firstName`和`lastName`，你在`let`绑定中使用这个来计算用户的`fullName`。要查看调用有副作用的函数时会发生什么，可以将用户的`fullName`打印到控制台:

```fs
    // A class that represents a user.
    // Its constructor takes three parameters: the user's
    // first name, last name, and a hash of his or her password.
    type User(firstName, lastName, passwordHash) =
        // Calculate the user's full name and store for later use
        let fullName = Printf.sprintf "%s %s" firstName lastName
        // Print the user's full name as object is being constructed.
        do printfn "User: %s" fullName

        // Hashes the user's password and checks it against
        // the known hash.
        member x.Authenticate(password) =
            let hashResult = hash (password, "sha1")
            passwordHash = hashResult

        // Retrieves the user's full name.
        member x.GetFullname() = fullName

```

请注意成员如何访问类“`let`绑定，以及成员`GetFullName`如何返回预先计算的`fullName`值。

通常需要能够在类中改变值。例如，您可能需要在`User`类中提供`ChangePassword`方法来重置用户的密码。F# 为您提供了两种方法来实现这一点。您可以使对象不可变——在这种情况下，您可以复制对象的参数，同时更改适当的值。这种方法通常被认为更适合函数式编程，但是如果对象有很多参数或者创建成本很高，就会有点不方便。例如，这样做可能计算成本很高，或者可能需要大量的输入/输出来构建它。以下示例说明了这种方法。请注意，在`ChangePassword`方法中，您如何调用`password`参数上的`hash`函数，将此函数与用户名一起传递给`User`对象的构造函数:

```fs
    // A class that represents a user.
    // Its constructor takes two parameters: the user's
    // name and a hash of his or her password.
    type User(name, passwordHash) =
        // Hashes the user's password and checks it against
        // the known hash.
        member x.Authenticate(password) =
            let hashResult = hash (password, "sha1")
            passwordHash = hashResult

        // Gets the user's logon message.
        member x.LogonMessage() =
            Printf.sprintf "Hello, %s" name

        // Creates a copy of the user with the password changed.
        member x.ChangePassword(password) =
            new User(name, hash password)

```

不可变对象的替代方法是使要更改的值可变。您可以通过将其绑定到可变的`let`绑定来实现这一点。您可以在下一个示例中看到这一点，其中您将类的参数`passwordHash`绑定到同名的可变`let`绑定:

```fs
    // A class that represents a user.
    // Its constructor takes two parameters: the user's
    // name and a hash of his or her password.
    type User(name, passwordHash) =
        // Store the password hash in a mutable let
        // binding so it can be changed later.
        let mutable passwordHash = passwordHash

        // Hashes the user's password and checks it against
        // the known hash.
        member x.Authenticate(password) =
            let hashResult = hash (password, "sha1")
            passwordHash = hashResult

        // Gets the user's logon message.
        member x.LogonMessage() =
            Printf.sprintf "Hello, %s" name

        // Changes the user's password.
        member x.ChangePassword(password) =
            passwordHash <- hash password

```

这意味着您可以自由地将`passwordHash`更新为`let`绑定，就像您在`ChangePassword`方法中所做的那样。

## 定义接口

接口只能包含抽象方法和属性，或者使用关键字`abstract`声明的成员。接口为所有实现它们的类定义了一个*契约*，公开了客户端可以使用的组件，同时将客户端与它们的实际实现隔离开来。一个类只能从一个基类继承，但是它可以实现任意数量的接口。因为实现接口的任何类都可以被视为接口类型，所以接口为多类继承提供了类似的好处，同时避免了这种方法的复杂性。

通过定义一个没有构造函数且所有成员都是抽象的类型来定义接口。以下示例定义了一个声明两种方法的接口:`Authenticate`和`LogonMessage`。注意界面名称是如何以大写字母`I`开头的。这是一个命名约定，在整个。你也应该在你的代码中遵循它。它将帮助其他程序员在阅读您的代码时区分类和接口:

```fs
    // An interface "IUser".
    type IUser =
        // Hashes the user's password and checks it against
        // the known hash.
        abstract Authenticate: evidence: string -> bool
        // Gets the user's logon message.
        abstract LogonMessage: unit -> string

    let logon (user: IUser) =
        // Authenticate user and print appropriate message.
        if user.Authenticate("badpass") then
            printfn "%s" (user.LogonMessage())
        else
            printfn "Logon failed"

```

示例的后半部分说明了接口的优势。您可以在不知道实现细节的情况下定义一个使用该接口的函数。您定义了一个`logon`函数，该函数接受一个`IUser`参数并使用它来执行登录。该功能将与`IUser`的任何实现一起工作。这在许多情况下非常有用；例如，它使您能够编写一组客户端代码，您可以在接口的几个不同实现中重用这些代码。

## 实现接口

要实现接口，使用关键字`interface`，然后是接口名称、关键字`with`，然后是实现接口成员的代码。您可以在成员定义前加上关键字`member`，但它们在其他方面与任何方法或属性的定义相同。您可以通过类或结构实现接口；您可以在下面的小节中详细了解如何创建类。

下一个示例定义、实现和使用接口。接口和你上一节实现的`IUser`接口一样；这里，您可以在名为`User`的类中实现它:

```fs
    open System.Web.Security
    // Give shorter name to password hashing method.
    let hash = FormsAuthentication.HashPasswordForStoringInConfigFile
    // An interface "IUser".
    type IUser =
        // Hashes the user's password and checks it against
        // the known hash.
        abstract Authenticate: evidence: string -> bool
        // Gets the user's logon message.
        abstract LogonMessage: unit -> string

    // A class that represents a user.
    // Its constructor takes two parameters: the user's
    // name and a hash of his or her password
    type User(name, passwordHash) =
        interface IUser with
            // Authenticate implementation.
            member x.Authenticate(password) =
                let hashResult = hash (password, "sha1")
                passwordHash = hashResult

            // LogonMessage implementation.
            member x.LogonMessage() =
                Printf.sprintf "Hello, %s" name

    // Create a new instance of the user.
    let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")
    // Cast to the IUser interface.
    let iuser = user :> IUser
    // Get the logon message.
    let logonMessage = iuser.LogonMessage()

    let logon (iuser: IUser) =
        // Authenticate the user and print the appropriate message.
        if iuser.Authenticate("badpass") then
            printfn "%s" logonMessage
        else
            printfn "Logon failed"   

    do logon user

```

请注意，在示例的中间，您第一次看到*铸造*；你可以在[铸造](#_Casting)一节的章节末尾找到更详细的铸造说明。但是现在这里有一个发生的事情的快速总结:标识符`user`通过向下转换操作符`:>`被转换到界面`I` `User`:

```fs
    // Create a new instance of the user.
    let user = User("Robert", "AF73C586F66FDC99ABF1EADB2B71C5E46C80C24A")
    // Cast to the IUser interface.
    let iuser = user :> IUser

```

这是必要的，因为接口是在 F# 中显式实现的。在使用方法`GetLogonMessage`之前，您必须有一个类型为`IUser`的标识符，而不仅仅是实现`IUser`的类的标识符。在示例的最后，您将以不同的方式解决这个问题。功能`logon`采用`IUser`类型的参数:

```fs
    let logon (iuser: IUser) =

```

当您用实现`IUser`的类调用`logon`时，该类被隐式降级为该类型。

## 铸造

强制转换是一种通过丢弃信息来显式改变值的静态类型的方式，称为*向上强制转换*；或者重新发现它，这就是众所周知的*下降*。在 F# 中，上转换和下转换有自己的运算符。类型层次结构从顶部的`obj`(或`System.Object`)开始，其所有后代都在它下面。向上转换将类型在层次结构中上移，而向下转换将类型在层次结构中下移。

上转换将值的静态类型更改为其祖先类型之一。这是安全的操作。编译器总是能够判断一个上转换是否会工作，因为它总是知道一个类型的所有祖先，所以它能够使用静态分析来确定一个上转换是否会成功。向上转换由冒号表示，后跟大于号(`:>`)。以下代码向您展示了如何使用上转换将`string`转换为`obj`:

```fs
    let myObject = ("This is a string" :> obj)

```

通常，在定义包含不同类型的集合时，必须使用上转换。如果不使用上转换，编译器将推断集合具有第一个元素的类型，如果集合中放置了其他类型的元素，则会给出编译错误。下一个示例演示如何创建控件数组，这是使用 Windows 窗体时的常见任务。请注意，您将所有的单个控件向上转换到它们的公共基类`Control`:

```fs
    open System.Windows.Forms

    let myControls =
        [| (new Button() :> Control);
           (new TextBox() :> Control);
           (new Label() :> Control) |]

```

向上转换还具有自动装箱任何值类型的效果。值类型保存在程序堆栈的内存中，而不是托管堆中。装箱意味着该值被推送到托管堆上，因此可以通过引用传递。下面的示例演示了如何框住一个值:

```fs
    let boxedInt = (1 :> obj)

```

向下转换将值的静态类型更改为其后代类型之一；因此，它恢复了被 upcast 隐藏的信息。向下转换是危险的，因为编译器没有任何方法来静态地确定一个类型的实例是否与其派生类型兼容。这意味着您可能会弄错，这将导致在运行时发出无效的强制转换异常(`System.InvalidCastException`)。由于向下转换的固有危险，许多开发人员更喜欢用模式匹配来代替它。NET 类型。尽管如此，向下转换在某些地方还是很有用的，因此可以使用向下转换运算符(由冒号、问号和大于号(`:?>`)组成)。下一个示例向您展示了如何使用向下转换:

```fs
    open System.Windows.Forms

    let moreControls =
        [| (new Button() :> Control);
           (new TextBox() :> Control) |]

    let control =
        let temp = moreControls.[0]
        temp.Text <- "Click Me!"
        temp

    let button =
        let temp = (control :?> Button)
        temp.DoubleClick.Add(fun e -> MessageBox.Show("Hello") |> ignore)
        temp

```

本示例创建一个由两个窗口控件对象组成的数组，并将它们向上转换到它们的基类`Control`。接下来，它将第一个控件绑定到`control`标识符；将此归结为它的特定类型，`Button`；并为其`DoubleClick`事件添加一个处理程序，该事件在`Control`类中不可用。

## 总结

您现在已经看到了如何在 F# 中使用三种主要编程范例中的两种，以及 F# 在任何混合风格的编码中有多灵活。