跳转至内容

F# 编程/类

来自 Wikibooks,开放世界中的开放书籍
上一页:运算符重载 索引 下一页:继承
F#:类和对象

在现实世界中,对象是“真实”的事物。猫、人、电脑和一卷胶带都是以有形意义存在的“真实”事物。当我们思考这些事物时,我们可以从多个属性的角度来广泛地描述它们

  • 属性:人有姓名,猫有四条腿,电脑有价格标签,胶带是粘性的。
  • 行为:人阅读报纸,猫整天睡觉,电脑处理数据,胶带将东西粘到其他东西上。
  • 类型/群体成员关系:员工是人的一种类型,猫是宠物,戴尔和 Mac 是电脑的类型,胶带属于更广泛的粘合剂家族。

在编程世界中,“对象”简单来说就是现实世界中某事物的模型。面向对象编程 (OOP) 存在是因为它允许程序员对现实世界的实体进行建模,并在代码中模拟它们的交互。就像它们在现实世界中的对应物一样,计算机编程中的对象具有属性和行为,并且可以根据其类型进行分类。

虽然我们当然可以创建代表猫、人、粘合剂的对象,但对象也可以代表不太具体的事物,例如银行账户或业务规则。

尽管 OOP 的范围已经扩展到包括一些高级概念,例如设计模式和应用程序的大规模体系结构,但此页面将保持简单,并将 OOP 的讨论限制在数据建模。

定义对象

[编辑 | 编辑源代码]

在创建对象之前,应定义其属性和函数。您在中定义对象的属性和方法。实际上,定义类的语法有两种:隐式语法和显式语法。

隐式类构造

[编辑 | 编辑源代码]

隐式类语法定义如下

type TypeName optional-type-arguments arguments [ as ident ] =
    [ inherit type { as base } ]
    [ let-binding | let-rec bindings ] *
    [ do-statement ] *
    [ abstract-binding | member-binding | interface-implementation ] *
方括号中的元素是可选的,后跟*的元素可以出现零次或多次。

上面的语法并不像看起来那么令人生畏。这是一个用隐式风格编写的简单类

type Account(number : int, holder : string) = class
    let mutable amount = 0m

    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
    
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end

上面的代码定义了一个名为Account的类,它具有三个属性和两个方法。让我们仔细看看以下内容

type Account(number : int, holder : string) = class

下划线代码称为类构造函数。构造函数是一种特殊的函数,用于初始化对象中的字段。在本例中,我们的构造函数定义了两个值numberholder,可以在类的任何地方访问它们。您可以使用new关键字并向构造函数传递适当的参数来创建Account的实例,如下所示

let bob = new Account(123456, "Bob’s Saving")

此外,让我们看看成员的定义方式

member x.Deposit(value) = amount <- amount + value

上面的x是当前作用域中对象的别名。大多数面向对象语言都提供隐式thisself变量来访问作用域中的对象,但 F# 要求程序员创建自己的别名。

创建了Account的实例后,可以使用.propertyName表示法访问其属性。以下是在 FSI 中的示例

> let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount;;

val printAccount : Account -> unit

> let bob = new Account(123456, "Bob’s Savings");;

val bob : Account

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 0
val it : unit = ()

> bob.Deposit(100M);;
val it : unit = ()

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 100
val it : unit = ()

> bob.Withdraw(29.95M);;
val it : unit = ()

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 70.05

让我们在一个真实的程序中使用这个类

open System

type Account(number : int, holder : string) = class
    let mutable amount = 0m

    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
    
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end

let homer = new Account(12345, "Homer")
let marge = new Account(67890, "Marge")

let transfer amount (source : Account) (target : Account) =
    source.Withdraw amount
    target.Deposit amount
    
let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount
    
let main() =
    let printAccounts() =
        [homer; marge] |> Seq.iter printAccount
    
    printfn "\nInializing account"
    homer.Deposit 50M
    marge.Deposit 100M
    printAccounts()
    
    printfn "\nTransferring $30 from Marge to Homer"
    transfer 30M marge homer
    printAccounts()
    
    printfn "\nTransferring $75 from Homer to Marge"
    transfer 75M homer marge
    printAccounts()
        
main()

该程序具有以下类型

type Account =
  class
    new : number:int * holder:string -> Account
    member Deposit : value:decimal -> unit
    member Withdraw : value:decimal -> unit
    member Amount : decimal
    member Holder : string
    member Number : int
  end
val homer : Account
val marge : Account
val transfer : decimal -> Account -> Account -> unit
val printAccount : Account -> unit

该程序输出以下内容

Initializing account
x.Number: 12345, x.Holder: Homer, x.Amount: 50
x.Number: 67890, x.Holder: Marge, x.Amount: 100

Transferring $30 from Marge to Homer
x.Number: 12345, x.Holder: Homer, x.Amount: 80
x.Number: 67890, x.Holder: Marge, x.Amount: 70

Transferring $75 from Homer to Marge
x.Number: 12345, x.Holder: Homer, x.Amount: 5
x.Number: 67890, x.Holder: Marge, x.Amount: 145

使用do关键字的示例

[编辑 | 编辑源代码]

do关键字用于构造函数后初始化。例如,要创建一个表示股票的对象,需要传入股票代码并在构造函数中初始化其余属性

open System
open System.Net

type Stock(symbol : string) = class
    let url =
        "http://download.finance.yahoo.com/d/quotes.csv?s=" + symbol + "&f=sl1d1t1c1ohgv&e=.csv"

    let mutable _symbol = String.Empty
    let mutable _current = 0.0
    let mutable _open = 0.0
    let mutable _high = 0.0
    let mutable _low = 0.0
    let mutable _volume = 0

    do
	(* We initialize our object in the do block *)

        let webClient = new WebClient()

       	(* Data comes back as a comma-separated list, so we split it
           on each comma *)
        let data = webClient.DownloadString(url).Split([|','|])

        _symbol <- data.[0]
        _current <- float data.[1]
        _open <- float data.[5]
        _high <- float data.[6]
        _low <- float data.[7]
        _volume <- int data.[8]
    
    member x.Symbol = _symbol
    member x.Current = _current
    member x.Open = _open
    member x.High = _high
    member x.Low = _low
    member x.Volume = _volume
end

let main() =
    let stocks = 
        ["msft"; "noc"; "yhoo"; "gm"]
        |> Seq.map (fun x -> new Stock(x))
        
    stocks |> Seq.iter (fun x -> printfn "Symbol: %s (%F)" x.Symbol x.Current)
    
main()

该程序具有以下类型

type Stock =
  class
    new : symbol:string -> Stock
    member Current : float
    member High : float
    member Low : float
    member Open : float
    member Symbol : string
    member Volume : int
  end

该程序输出以下内容(您的输出可能会有所不同)

Symbol: "MSFT" (19.130000)
Symbol: "NOC" (43.240000)
Symbol: "YHOO" (12.340000)
Symbol: "GM" (3.660000)
注意:在类定义中可以有任意数量的do语句,尽管通常不需要超过一个。

显式类定义

[编辑 | 编辑源代码]

以显式风格编写的类遵循以下格式

type TypeName =
    [ inherit type ]
    [ val-definitions ]
    [ new ( optional-type-arguments arguments ) [ as ident ] =
      { field-initialization }
      [ then constructor-statements ]
    ] *
    [ abstract-binding | member-binding | interface-implementation ] *

这是一个使用显式语法定义的类

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
    
    new (x1, y1, x2, y2) =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2}
            
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

每个val在我们的对象中定义一个字段。与其他面向对象语言不同,F# 不会隐式地将类中的字段初始化为任何值。相反,F# 要求程序员定义一个构造函数,并使用值显式初始化对象中的每个字段。

我们可以使用then块执行一些构造函数后处理,如下所示

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
    
    new (x1, y1, x2, y2) as this =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2;}
        then
            printfn "Line constructor: {(%F, %F), (%F, %F)}, Line.Length: %F"
                this.X1 this.Y1 this.X2 this.Y2 this.Length
            
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

请注意,我们必须在构造函数后添加一个别名(new (x1, y1, x2, y2) as this),以访问正在构造的对象的字段。每次创建Line对象时,构造函数都会打印到控制台。我们可以使用 fsi 演示这一点

> let line = new Line(1.0, 1.0, 4.0, 2.5);;

val line : Line

Line constructor: {(1.000000, 1.000000), (4.000000, 2.500000)}, Line.Length: 3.354102

使用两个构造函数的示例

[编辑 | 编辑源代码]

由于构造函数是显式定义的,因此我们可以选择提供多个构造函数。

open System
open System.Net

type Car = class
    val used : bool
    val owner : string
    val mutable mileage : int
    
    (* first constructor *)
    new (owner) =
        { used = false;
            owner = owner;
            mileage = 0 }
    
    (* another constructor *)
    new (owner, mileage) =
        { used = true;
            owner = owner;
            mileage = mileage }
end

let main() =
    let printCar (c : Car) =
        printfn "c.used: %b, c.owner: %s, c.mileage: %i" c.used c.owner c.mileage
    
    let stevesNewCar = new Car("Steve")
    let bobsUsedCar = new Car("Bob", 83000)
    let printCars() =
        [stevesNewCar; bobsUsedCar] |> Seq.iter printCar
    
    printfn "\nCars created"
    printCars()
    
    printfn "\nSteve drives all over the state"   
    stevesNewCar.mileage <- stevesNewCar.mileage + 780
    printCars()
    
    printfn "\nBob commits odometer fraud"
    bobsUsedCar.mileage <- 0
    printCars()
    
main()

该程序具有以下类型

type Car =
  class
    val used: bool
    val owner: string
    val mutable mileage: int
    new : owner:string * mileage:int -> Car
    new : owner:string -> Car
  end

请注意,我们的val字段包含在类定义的公共接口中。

该程序输出以下内容

Cars created
c.used: false, c.owner: Steve, c.mileage: 0
c.used: true, c.owner: Bob, c.mileage: 83000

Steve drives all over the state
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 83000

Bob commits odometer fraud
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 0

隐式和显式语法之间的差异

[编辑 | 编辑源代码]

您可能已经猜到,两种语法之间的主要区别与构造函数有关:显式语法强制程序员提供显式构造函数,而隐式语法将主构造函数与类体融合在一起。但是,还有一些其他的细微差别

  • 显式语法不允许程序员声明letdo绑定。
  • 即使val字段可以在隐式语法中使用,它们也必须具有属性[<DefaultValue>]并且是可变的。在这种情况下,使用let绑定更方便。当需要公开时,可以添加公共member访问器。
  • 在隐式语法中,主构造函数参数在整个类体中可见。通过使用此功能,无需编写将构造函数参数复制到实例成员的代码。
  • 虽然两种语法都支持多个构造函数,但当使用隐式语法声明其他构造函数时,它们必须调用主构造函数。在显式语法中,所有构造函数都使用 new() 声明,并且没有需要从其他构造函数中引用的主构造函数。
具有主(隐式)构造函数的类 仅具有显式构造函数的类
// The class body acts as a constructor
type Car1(make : string, model : string) = class
    // x.Make and x.Model are property getters
    // (explained later in this chapter)
    // Notice how they can access the
    // constructor parameters directly
    member x.Make = make
    member x.Model = model

    // This is an extra constructor.
    // It calls the primary constructor
    new () = Car1("default make", "default model")
end
type Car2 = class
    // In this case, we need to declare
    // all fields and their types explicitly 
    val private make : string
    val private model : string

    // Notice how field access differs
    // from parameter access
    member x.Make = x.make
    member x.Model = x.model

    // Two constructors
    new (make : string, model : string) = {
        make = make
        model = model
    }
    new () = {
        make = "default make"
        model = "default model"
    }
end

通常,程序员可以使用隐式或显式语法来定义类。但是,在实践中隐式语法使用得更多,因为它往往会导致更短、更易读的类定义。

类推断

[编辑 | 编辑源代码]

F# 的#light语法允许程序员省略类定义中的classend关键字,此功能通常称为类推断类型种类推断。例如,以下类定义之间没有区别

类推断 显式类
type Product(make : string, model : string) =
    member x.Make = make
    member x.Model = model
type Car(make : string, model : string) = class    
    member x.Make = make
    member x.Model = model
end

这两个类都编译成相同的字节码,但是使用类推断的代码允许我们省略一些不必要的关键字。

类推断和显式类样式都被认为是可以接受的。至少,在编写 F# 库时,不要使用类推断定义一半的类,而使用显式类样式定义另一半的类——选择一种样式,并在整个项目中所有类中一致地使用它。

类成员

[编辑 | 编辑源代码]

实例成员和静态成员

[编辑 | 编辑源代码]

您可以向对象添加两种类型的成员

  • 实例成员,只能从使用new关键字创建的对象实例中调用。

  • 静态成员,不与任何对象实例关联。

以下类包含一个静态方法和一个实例方法

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
end

我们使用className.methodName的形式调用静态方法。我们通过创建类的实例并使用classInstance.methodName调用方法来调用实例方法。以下是在fsi中的演示

> SomeClass.SomeStaticMethod;; (* invoking static method *)
val it : string = "This is a static method"

> SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)

  SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
  ^^^^^^^^^^^^^^^

stdin(78,1): error FS0191: property 'Prop' is not static.

> let instance = new SomeClass(5);;

val instance : SomeClass

> instance.Prop;; (* now we have an instance, we can call our instance method *)
val it : int = 5

> instance.SomeStaticMethod;; (* can't invoke static method from instance *)

  instance.SomeStaticMethod;; (* can't invoke static method from instance *)
  ^^^^^^^^^^^^^^^^^^^^^^^^^^

stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.

当然,我们也可以从传递静态方法中的对象调用实例方法,例如,假设我们在上面定义的对象中添加了一个Copy方法

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
    static member Copy (source : SomeClass) = new SomeClass(source.Prop)
end

我们可以在fsi中试验此方法

> let instance = new SomeClass(10);;

val instance : SomeClass

> let shallowCopy = instance;; (* copies pointer to another symbol *)

val shallowCopy : SomeClass

> let deepCopy = SomeClass.Copy instance;; (* copies values into a new object *)

val deepCopy : SomeClass

> open System;;

> Object.ReferenceEquals(instance, shallowCopy);;
val it : bool = true

> Object.ReferenceEquals(instance, deepCopy);;
val it : bool = false

Object.ReferenceEqualsSystem.Object类上的一个静态方法,它确定两个对象实例是否为同一个对象。如上所示,我们的Copy方法接受一个SomeClass的实例并访问其Prop属性。

何时应该使用静态方法而不是实例方法?

当.NET框架的设计人员设计System.String类时,他们必须决定Length方法应该放在哪里。他们可以选择将属性设为实例方法(s.Length)或设为静态方法(String.GetLength(s))。.NET设计人员选择将Length设为实例方法,因为它是一个字符串的内在属性。

另一方面,String类还有一些静态方法,包括String.Concat,它接受一个字符串列表并将它们连接在一起。连接字符串与实例无关,它不依赖于任何特定字符串的实例成员。

以下一般原则适用于所有面向对象语言

  • 实例成员应该用于访问对象的内在属性,例如stringInstance.Length
  • 当实例方法依赖于特定对象实例的状态时,应该使用实例方法,例如stringInstance.Contains
  • 当预计程序员希望在派生类中重写该方法时,应该使用实例方法。
  • 静态方法不应依赖于对象的特定实例,例如Int32.TryParse
  • 只要输入保持不变,静态方法应该返回相同的值。
  • 常量,即对于任何类实例都不变的值,应该声明为静态成员,例如System.Boolean.TrueString

获取器和设置器

[编辑 | 编辑源代码]

获取器和设置器是特殊的函数,允许程序员使用方便的语法读取和写入成员。获取器和设置器使用以下格式编写

    member alias.PropertyName
        with get() = some-value
        and set(value) = some-assignment

这是一个使用fsi的简单示例

> type IntWrapper() = class
    let mutable num = 0
    
    member x.Num
        with get() = num
        and set(value) = num <- value
end;;

type IntWrapper =
  class
    new : unit -> IntWrapper
    member Num : int
    member Num : int with set
  end

> let wrapper = new IntWrapper();;

val wrapper : IntWrapper

> wrapper.Num;;
val it : int = 0

> wrapper.Num <- 20;;
val it : unit = ()

> wrapper.Num;;
val it : int = 20

获取器和设置器用于将私有成员暴露给外部世界。例如,我们的Num属性允许用户读取/写入内部num变量。由于获取器和设置器是美化的函数,因此我们可以在写入值到内部变量之前使用它们来清理输入。例如,我们可以通过修改我们的类如下,修改我们的IntWrapper类来将我们的值限制在0到10之间

type IntWrapper() = class
    let mutable num = 0
    
    member x.Num
        with get() = num
        and set(value) =
            if value > 10 || value < 0 then
                raise (new Exception("Values must be between 0 and 10"))
            else
                num <- value
end

我们可以在fsi中使用此类

> let wrapper = new IntWrapper();;

val wrapper : IntWrapper

> wrapper.Num <- 5;;
val it : unit = ()

> wrapper.Num;;
val it : int = 5

> wrapper.Num <- 20;;
System.Exception: Values must be between 0 and 10
   at FSI_0072.IntWrapper.set_Num(Int32 value)
   at <StartupCode$FSI_0076>.$FSI_0076._main()
stopped due to error

向记录和联合添加成员

[编辑 | 编辑源代码]

向记录和联合类型添加成员也同样容易。

记录示例

> type Line =
    { X1 : float; Y1 : float;
        X2 : float; Y2 : float }
    with    
        member x.Length =
            let sqr x = x * x
            sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2))
        
        member x.ShiftH amount =
            { x with X1 = x.X1 + amount; X2 = x.X2 + amount }
            
        member x.ShiftV amount =
            { x with Y1 = x.Y1 + amount; Y2 = x.Y2 + amount };;

type Line =
  {X1: float;
   Y1: float;
   X2: float;
   Y2: float;}
  with
    member ShiftH : amount:float -> Line
    member ShiftV : amount:float -> Line
    member Length : float
  end

> let line = { X1 = 1.0; Y1 = 2.0; X2 = 5.0; Y2 = 4.5 };;

val line : Line

> line.Length;;
val it : float = 4.716990566

> line.ShiftH 10.0;;
val it : Line = {X1 = 11.0;
                 Y1 = 2.0;
                 X2 = 15.0;
                 Y2 = 4.5;}

> line.ShiftV -5.0;;
val it : Line = {X1 = 1.0;
                 Y1 = -3.0;
                 X2 = 5.0;
                 Y2 = -0.5;}

联合示例

> type shape =
    | Circle of float
    | Rectangle of float * float
    | Triangle of float * float
    with
        member x.Area =
            match x with
            | Circle(r) -> Math.PI * r * r
            | Rectangle(b, h) -> b * h
            | Triangle(b, h) -> b * h / 2.0
            
        member x.Scale value =
            match x with
            | Circle(r) -> Circle(r + value)
            | Rectangle(b, h) -> Rectangle(b + value, h + value)
            | Triangle(b, h) -> Triangle(b + value, h + value);;

type shape =
  | Circle of float
  | Rectangle of float * float
  | Triangle of float * float
  with
    member Scale : value:float -> shape
    member Area : float
  end

> let mycircle = Circle(5.0);;

val mycircle : shape

> mycircle.Area;;
val it : float = 78.53981634

> mycircle.Scale(7.0);;
val it : shape = Circle 12.0

泛型类

[编辑 | 编辑源代码]

可以创建接受泛型类型的类

type 'a GenericWrapper(initialVal : 'a) = class
    let mutable internalVal = initialVal
    
    member x.Value
        with get() = internalVal
        and set(value) = internalVal <- value
end

我们可以在FSI中使用此类,如下所示

> let intWrapper = new GenericWrapper<_>(5);;

val intWrapper : int GenericWrapper

> intWrapper.Value;;
val it : int = 5

> intWrapper.Value <- 20;;
val it : unit = ()

> intWrapper.Value;;
val it : int = 20

> intWrapper.Value <- 2.0;; (* throws an exception *)

  intWrapper.Value <- 2.0;; (* throws an exception *)
  --------------------^^^^

stdin(156,21): error FS0001: This expression has type
	float
but is here used with type
	int.

> let boolWrapper = new GenericWrapper<_>(true);;

val boolWrapper : bool GenericWrapper

> boolWrapper.Value;;
val it : bool = true

泛型类帮助程序员将类泛化以对多种不同类型进行操作。它们的使用方式与已经在F#中看到的其他所有泛型类型(如列表、集合、映射和联合类型)基本相同。

模式匹配对象

[编辑 | 编辑源代码]

虽然我们不能像对列表和联合类型那样完全以相同的方式根据对象的结构来匹配对象,但F#允许程序员使用以下语法根据类型进行匹配

match arg with
| :? type1 -> expr
| :? type2 -> expr

这是一个使用类型测试的示例

type Cat() = class
    member x.Meow() = printfn "Meow"
end

type Person(name : string) = class
    member x.Name = name
    member x.SayHello() = printfn "Hi, I'm %s" x.Name
end

type Monkey() = class
    member x.SwingFromTrees() = printfn "swinging from trees"
end

let handlesAnything (o : obj) =
    match o with
    | null -> printfn "<null>"
    | :? Cat as cat -> cat.Meow()
    | :? Person as person -> person.SayHello()
    | _ -> printfn "I don't recognize type '%s'" (o.GetType().Name)

let main() =
    let cat = new Cat()
    let bob = new Person("Bob")
    let bill = new Person("Bill")
    let phrase = "Hello world!"
    let monkey = new Monkey()
    
    handlesAnything cat
    handlesAnything bob
    handlesAnything bill
    handlesAnything phrase
    handlesAnything monkey
    handlesAnything null

main()

此程序输出

Meow
Hi, I'm Bob
Hi, I'm Bill
I don't recognize type 'String'
I don't recognize type 'Monkey'
<null>
上一页:运算符重载 索引 下一页:继承
华夏公益教科书