一位同事审核了我的代码并询问我为何使用以下模式:
type MyType() =
member this.TestMethod() =
let a = 1
// do some work
let a = 2 // another value of a from the work result
()
我告诉他这是一个糟糕的模式,我的意图是使用let mutable a = ...
。但后来他问为什么在F#类中有可能有这样的顺序绑定,而在模块或.fsx
文件中是不可能的?实际上,我们正在改变一个不可变的值!
我回答说,在模块或.fsx文件中,let绑定变成了静态方法,因此,具有相同名称的绑定将与具有相同名称的两个类属性相同的方式发生冲突是直截了当的。但是我不知道为什么在方法中这是可能的!
在C#中,我发现范围变量很有用,特别是在单元测试中,当我想通过复制粘贴代码来为测试用例组成一些不同的值时:
{
var a = 1;
Assert.IsTrue(a < 2);
}
{
var a = 42;
Assert.AreEqual(42, a);
}
在F#中,我们不仅可以重复相同的let绑定,而且可以将一个不可变的绑定更改为一个可变的绑定,然后以通常的方式改变它:
type MyType() =
member this.TestMethod() =
let a = 1
// do some work
let a = 2 // another value of a from the work result
let mutable a = 3
a <- 4
()
为什么允许我们在F#方法中重复let绑定?我应该如何向一个刚接触F#的人解释这一点,并问“什么是不可变变量以及我为什么要改变它?”
就个人而言,我对设计选择和权衡取舍感兴趣吗?我很满意不同范围很容易检测到的情况,例如:当一个变量在构造函数中,然后我们在正文中重新定义它,或者当我们在for
/ while
循环中定义一个新的绑定时。但是同一级别的两个连续绑定有点违反直觉。感觉就像我应该在每行的末尾添加in
,就像在详细语法中解释范围一样,这样虚拟in
类似于C#的{}
答案 0 :(得分:8)
我认为重要的是要解释F#中有一个与C#不同的东西。变量阴影不会替换符号 - 它只是定义一个恰好与现有符号同名的新符号,这使得无法访问旧符号。
当我向人们解释这个时,我通常会使用这样的例子 - 假设我们有一段代码在C#中使用变异进行一些计算:
var message = "Hello";
message = message + " world";
message = message + "!";
这很好,因为我们可以逐步构建消息。现在,我们怎么能在没有突变的情况下做到诀窍是在每一步定义新变量:
let message1 = "Hello";
let message2 = message1 + " world";
let message3 = message2 + "!";
这有效 - 但我们并不真正需要在构建过程中定义的临时状态。因此,在F#中,您可以使用变量阴影来隐藏您不再关心的状态:
let message = "Hello";
let message = message + " world";
let message = message + "!";
现在,这意味着完全相同的事情 - 你可以很好地向使用Visual F#Power Tools的人们展示这一点,它强调所有出现的符号 - 所以你会看到符号是不同的(即使他们有同名)。
答案 1 :(得分:6)
F#代码:
let a = 1
let a = 2
let a = 3
a + 1
只是一个浓缩(又名&#34;轻&#34;)版本:
let a = 1 in
let a = 2 in
let a = 3 in
a + 1
(等同于)C#等价物将是这样的:
var a = 1;
{
var a = 2;
{
var a = 3;
return a + 1;
}
}
在嵌套作用域的上下文中,C#不允许隐藏名称, 但是F#和几乎所有其他语言都可以。
事实上,根据font of all knowledge 在这种情况下,C#是少数明确禁止阴影的语言之一,这是不寻常的。
这可能是因为C#是一种相对较新的语言。 OTOH F#从OCaml复制了大部分设计, 反过来,它基于较旧的语言,所以从某种意义上说,F#的设计是#34;更老的&#34;而不是C#。
答案 2 :(得分:4)
这不是我每天都使用的功能,但有时我觉得它很有用,特别是在进行基于属性的测试时。这是我最近在使用FsCheck执行Tennis Kata时所做的一个示例:
[<Property>]
let ``Given player has thirty points, when player wins, then the new score is correct``
(points : PointsData)
(player : Player) =
let points = points |> pointTo player Thirty
let actual = points |> scorePoints player
let expected =
Forty {
Player = player
OtherPlayerPoint = (points |> pointFor (other player)) }
expected =? actual
在这里,我正在使用新值隐藏points
。
原因是我想明确测试一名玩家已经获得Thirty
分并再次获胜的情况,无论其他玩家有多少分。
PointsData
的定义如下:
type Point = Love | Fifteen | Thirty
type PointsData = { PlayerOnePoint : Point; PlayerTwoPoint : Point }
但是FsCheck会给我PointsData
的各种值,而不仅仅是其中一个玩家Thirty
的值。
这意味着作为函数参数到达的points
并不真正代表我感兴趣的测试用例。为了防止意外使用,我在测试中隐藏了值,同时仍然使用输入作为种子,我可以在其上构建实际的测试用例值。
在这种情况下,阴影通常很有用。