形参和实参
本主题介绍对定义形参并将实参传递给函数、方法和属性的语言支持。 其中包括有关如何按引用传递以及如何定义和使用可采用可变数量的实参的方法的信息。
形参和实参
术语“形参”用于描述预期提供的值的名称。 术语“实参”用于为每个形参提供的值。
形参可以采用元组或扩充形式进行指定,也可以采用二者的某种组合进行指定。 可以使用显式形参名称传递实参。 方法的形参可指定为可选,并向其提供默认值。
形参模式
一般而言,提供给函数和方法的形参是用空格分隔的模式。 这意味着,在原则上,匹配表达式中所述的任何模式都可以在函数或成员的形参列表中使用。
方法通常使用传递实参的元组形式。 从其他 .NET 语言的角度来看,这可实现更明确的结果,因为元组形式与在 .NET 方法中传递实参的方式相匹配。
扩充形式通常与使用 let
绑定创建的函数一起使用。
以下伪代码演示了元组和扩充实参的示例。
// Tuple form.
member this.SomeMethod(param1, param2) = ...
// Curried form.
let function1 param1 param2 = ...
当一些实参采用元组,而另一些实参不采用元组时,可以使用组合形式。
let function2 param1 (param2a, param2b) param3 = ...
其他模式也可在形参列表中使用,但如果形参模式与所有可能的输入不匹配,则运行时可能出现不完全匹配项。 当实参的值与形参列表中指定的模式不匹配时,会生成异常 MatchFailureException
。 当形参模式允许出现不完全匹配项时,编译器会发出警告。 至少还有一种模式通常可用于形参列表,那便是通配符模式。 当只想忽略提供的任何实参时,可在形参列表中使用通配符模式。 下面的代码演示了如何在实参列表中使用通配符模式。
let makeList _ = [ for i in 1 .. 100 -> i * i ]
// The arguments 100 and 200 are ignored.
let list1 = makeList 100
let list2 = makeList 200
如果对通常作为字符串数组提供的命令行实参不感兴趣,则每当不需要传入实参(例如在程序的主入口点中)时,通配符模式可能会非常有用,如以下代码所示。
[<EntryPoint>]
let main _ =
printfn "Entry point!"
0
有时在实参中使用的其他模式是 as
模式,以及与可区分联合关联的标识符模式和活动模式。 可以按如下所示使用单用例可区分联合模式。
type Slice = Slice of int * int * string
let GetSubstring1 (Slice(p0, p1, text)) =
printfn "Data begins at %d and ends at %d in string %s" p0 p1 text
text[p0..p1]
let substring = GetSubstring1 (Slice(0, 4, "Et tu, Brute?"))
printfn "Substring: %s" substring
输出如下所示。
Data begins at 0 and ends at 4 in string Et tu, Brute?
Et tu
活动模式可以用作形参,例如在将实参转换为所需格式时,如以下示例所示:
type Point = { x : float; y : float }
let (| Polar |) { x = x; y = y} =
( sqrt (x*x + y*y), System.Math.Atan (y/ x) )
let radius (Polar(r, _)) = r
let angle (Polar(_, theta)) = theta
可以使用 as
模式将匹配值存储为本地值,如以下代码行所示。
let GetSubstring2 (Slice(p0, p1, text) as s) = s
偶尔使用的另一种模式是函数,它通过以函数体的形式提供立即对隐式实参执行模式匹配的 lambda 表达式,使最后一个实参保持未命名。 下面的代码行便是这种情况的示例。
let isNil = function [] -> true | _::_ -> false
此代码定义一个函数,它采用泛型列表,如果该列表为空,则返回 true
,否则返回 false
。 使用此类方法会使代码更难阅读。
涉及不完全匹配项的模式偶尔会十分有用,例如,如果你知道程序中的列表只有三个元素,则可以在形参列表中使用类似于下面这样的模式。
let sum [a; b; c;] = a + b + c
使用具有不完全匹配项的模式最好是保留用于快速原型设计和其他临时用途。 编译器会对此类代码发出警告。 此类模式无法涵盖所有可能输入的一般用例,因此不适用于组件 API。
命名实参
方法的实参可以通过逗号分隔实参列表中的位置进行指定,也可以通过提供名称(后跟等号和要传入的值)来显式传递给方法。 如果通过提供名称进行指定,则其显示顺序可以与声明中使用的顺序不同。
命名实参可以使代码更具可读性且更适应于 API 中某些类型的更改,例如方法形参的重新排序。
命名实参仅允许用于方法,不允许用于 let
绑定函数、函数值或 lambda 表达式。
下面的代码示例演示了命名实参的用法。
type SpeedingTicket() =
member this.GetMPHOver(speed: int, limit: int) = speed - limit
let CalculateFine (ticket : SpeedingTicket) =
let delta = ticket.GetMPHOver(limit = 55, speed = 70)
if delta < 20 then 50.0 else 100.0
let ticket1 : SpeedingTicket = SpeedingTicket()
printfn "%f" (CalculateFine ticket1)
在类构造函数的调用中,可以使用类似于命名实参的语法来设置类的属性值。 下面的示例演示了此语法。
type Account() =
let mutable balance = 0.0
let mutable number = 0
let mutable firstName = ""
let mutable lastName = ""
member this.AccountNumber
with get() = number
and set(value) = number <- value
member this.FirstName
with get() = firstName
and set(value) = firstName <- value
member this.LastName
with get() = lastName
and set(value) = lastName <- value
member this.Balance
with get() = balance
and set(value) = balance <- value
member this.Deposit(amount: float) = this.Balance <- this.Balance + amount
member this.Withdraw(amount: float) = this.Balance <- this.Balance - amount
let account1 = new Account(AccountNumber=8782108,
FirstName="Darren", LastName="Parker",
Balance=1543.33)
有关详细信息,请参阅构造函数 (F#)。
用于调用属性资源库的相同技术也适用于任何对象返回方法(如工厂方法):
type Widget() =
member val Width = 1 with get,set
member val Height = 1 with get,set
type WidgetFactory =
static member MakeNewWidget() =
new Widget()
static member AdjustWidget(w: Widget) =
w
let w = WidgetFactory.MakeNewWidget(Width=10)
w.Width // = 10
w.Height // = 1
WidgetFactory.AdjustWidget(w, Height=10)
w.Height // = 10
请注意,这些成员可以执行任意工作,该语法实际上是在返回最终值之前调用属性资源库的速记法。
可选参数
可以通过在形参名称前面使用问号来指定方法的可选形参。 从被调用方的角度来看,可选形参解释为 F# 选项类型,因此可以通过将 match
表达式与 Some
和 None
一起使用,以查询选项类型的常规方式来查询它们。 可选形参仅允许用于成员,而不允许用于使用 let
绑定创建的函数。
可以按形参名称将现有可选值传递给方法,例如 ?arg=None
或 ?arg=Some(3)
或 ?arg=arg
。 在生成将可选实参传递给另一个方法的方法时,这可能会非常有用。
还可使用函数 defaultArg
,它会设置可选实参的默认值。 defaultArg
函数将可选形参用作第一个实参,将默认值用作第二个实参。
下面的示例阐释了可选形参的用法。
type DuplexType =
| Full
| Half
type Connection(?rate0 : int, ?duplex0 : DuplexType, ?parity0 : bool) =
let duplex = defaultArg duplex0 Full
let parity = defaultArg parity0 false
let mutable rate = match rate0 with
| Some rate1 -> rate1
| None -> match duplex with
| Full -> 9600
| Half -> 4800
do printfn "Baud Rate: %d Duplex: %A Parity: %b" rate duplex parity
let conn1 = Connection(duplex0 = Full)
let conn2 = Connection(duplex0 = Half)
let conn3 = Connection(300, Half, true)
let conn4 = Connection(?duplex0 = None)
let conn5 = Connection(?duplex0 = Some(Full))
let optionalDuplexValue : option<DuplexType> = Some(Half)
let conn6 = Connection(?duplex0 = optionalDuplexValue)
输出如下所示。
Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 4800 Duplex: Half Parity: false
Baud Rate: 300 Duplex: Half Parity: true
Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 4800 Duplex: Half Parity: false
若要实现 C# 和 Visual Basic 互操作,可以使用 F# 中的属性 [<Optional; DefaultParameterValue<(...)>]
,以便调用方将实参视为可选。 这等效于在 C# 中将实参定义为可选,如 MyMethod(int i = 3)
所示。
open System
open System.Runtime.InteropServices
type C =
static member Foo([<Optional; DefaultParameterValue("Hello world")>] message) =
printfn $"{message}"
还可以将新对象指定为默认形参值。 例如,Foo
成员可以改为将可选 CancellationToken
作为输入:
open System.Threading
open System.Runtime.InteropServices
type C =
static member Foo([<Optional; DefaultParameterValue(CancellationToken())>] ct: CancellationToken) =
printfn $"{ct}"
作为实参向 DefaultParameterValue
提供的值必须与形参的类型匹配。 例如,不允许以下操作:
type C =
static member Wrong([<Optional; DefaultParameterValue("string")>] i:int) = ()
在这种情况下,编译器会生成警告,并完全忽略这两个属性。 请注意,默认值 null
需要是带注释类型,否则编译器会推断错误的类型,即 [<Optional; DefaultParameterValue(null:obj)>] o:obj
。
按引用传递
按引用传递 F# 值涉及 byrefs,它们是托管指针类型。 有关使用哪种类型的指导如下所示:
- 如果只需读取指针,则使用
inref<'T>
。 - 如果只需向指针写入,则使用
outref<'T>
。 - 如果需要对指针进行读取和写入,请使用
byref<'T>
。
let example1 (x: inref<int>) = printfn $"It's %d{x}"
let example2 (x: outref<int>) = x <- x + 1
let example3 (x: byref<int>) =
printfn $"It's %d{x}"
x <- x + 1
let test () =
// No need to make it mutable, since it's read-only
let x = 1
example1 &x
// Needs to be mutable, since we write to it
let mutable y = 2
example2 &y
example3 &y // Now 'y' is 3
由于形参是指针,并且值是可变的,因此对值进行的任何更改在执行函数后会保留。
可以使用元组作为返回值,以存储 .NET 库方法中的任何 out
形参。 或者,可以将 out
形参视为 byref
形参。 下面的代码示例阐释了这两种方式。
// TryParse has a second parameter that is an out parameter
// of type System.DateTime.
let (b, dt) = System.DateTime.TryParse("12-20-04 12:21:00")
printfn "%b %A" b dt
// The same call, using an address of operator.
let mutable dt2 = System.DateTime.Now
let b2 = System.DateTime.TryParse("12-20-04 12:21:00", &dt2)
printfn "%b %A" b2 dt2
参数数组
有时需要定义采用任意数量的异类类型形参的函数。 创建所有可能的重载方法来考虑可能使用的所有类型并不可行。 .NET 实现通过形参数组功能为此类方法提供支持。 可以使用任意数量的形参提供在其签名中采用形参数组的方法。 形参会放入数组中。 数组元素的类型确定可传递给函数的形参类型。 如果定义将 System.Object
作为元素类型的形参数组,则客户端代码可以传递任何类型的值。
在 F# 中,形参数组只能在方法中定义。 它们不能用于独立函数或是在模块中定义的函数。
使用 ParamArray
属性定义形参数组。 ParamArray
属性只能应用于最后一个形参。
下面的代码演示了调用采用形参数组的 .NET 方法,以及在 F# 中定义具有采用形参数组的方法的类型。
open System
type X() =
member this.F([<ParamArray>] args: Object[]) =
for arg in args do
printfn "%A" arg
[<EntryPoint>]
let main _ =
// call a .NET method that takes a parameter array, passing values of various types
Console.WriteLine("a {0} {1} {2} {3} {4}", 1, 10.0, "Hello world", 1u, true)
let xobj = new X()
// call an F# method that takes a parameter array, passing values of various types
xobj.F("a", 1, 10.0, "Hello world", 1u, true)
0
在项目中运行时,上面代码的输出如下所示:
a 1 10 Hello world 1 True
"a"
1
10.0
"Hello world"
1u
true