F#内置不可变性相对于C#的优势是什么?

26
  1. 我听说 F# 本身支持不可变性,但是 C# 不能复制它的部分特性。使用 F# 不可变数据类型,你获得了什么,而 C# 不可变数据类型没有的呢?

  2. 另外,在 F# 中是否没有创建可变数据的方法?一切都是不可变的吗?

  3. 如果在一个应用程序中同时使用 C# 和 F#,能否在 C# 中更改不可变的 F# 数据?或者你只需要创建新的 C# 类型,使用不可变的 F# 数据并替换指向这些数据的引用。

7个回答

26
  1. F#的工作方式使得使用不可变数据更加容易,但在C#中也可以做到这一点,只是可能没有那么简洁的语法。

  2. F#使用mutable关键字支持可变数据。F#不是纯函数式语言,它还支持像循环等命令式构造。

  3. 不可以。不可变类型是真正的不可变的,.NET类型系统适用于两种语言。当然除了反射和内存hack之外,你也无法从C#中修改不可变数据。


1
技术上说,如果您的代码具有足够的权限来覆盖可访问性检查,则可以在 F# 或 C# 中通过反射更改支持不可变属性的(私有)字段。 - Pavel Minaev
1
@Pavel:确实。我所说的“内存黑科技”是指间接地改变东西的广义概念。 - Mehrdad Afshari
谢谢,那么 F# 使用一种新的 IL 关键字,C# 可以理解并将其标记为真正的不可变值? - Joan Venge
7
@Joan:不,F#没有使用任何新的IL设施。它只是将所有字段设置为私有,并且仅公开属性的getter而不是setter。此外,这些字段本身可能也被标记为“readonly”(我没有检查过)。 - Pavel Minaev

17

Mehrdad已经回答了你的所有问题,但是只是为了进一步解释#1- 差异在于您需要编写的代码量。例如,这是C#中只读点的定义和示例用法:

class Point
{
    private readonly int x, y;

    public int X { get { return x; } }

    public int Y { get { return y; } }

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

var p = new Point(1, 2);
你可以使用自动属性来缩短代码,但这样只能确保客户端的不可变性,无法确保类定义内部的不可变性。
在F#中,你只需要写:
type Point = { x: int; y: int }
let p = { x = 1; y = 2 }

如果你希望它是可变的:

type Point = { mutable x: int; mutable y: int }
let p = { x = 1; y = 2 }
p.x <- 3
p.y <- 4

2
每当你通过赋值改变一个值(可变的东西),你必须使用 <-。 - gradbot
6
由于 F# 默认是不可变的,所以改变操作符 <- 与赋值操作符 = 是不同的。这可以防止你在无意中改变状态,因为你必须明确地表达它。同时,这种语法具有不太方便的语法结构,可以避免在没有绝对必要的情况下使用可变值。 - Joel Mueller
1
@Joan:F#(以及OCaml)更喜欢区分将值赋给变量(<-)和将值绑定到符号(=)的概念。 - Mehrdad Afshari
1
由于实际实现是IL重写器,它不关心用于生成程序集的语言,因此您应该可以轻松使用合约(也就是说,我实际上还没有尝试过)。它可以使用LINQ,但这取决于您的确切意思。您可以直接调用类“Enumerable”上的静态方法,但F#不将它们视为扩展方法,并且与C#中的语法糖不同,在F#中也没有它们的语法糖。另一方面,F#具有其自己更丰富的序列操作库-请参见http://msdn.microsoft.com/en-us/library/ee353635(VS.100).aspx。 - Pavel Minaev
1
F#还提供了自己的语法糖,用于序列推导式,这实际上相当于匿名迭代器(在lambda内部使用yield return),如果在C#中可能存在的话。请参阅http://msdn.microsoft.com/en-us/library/dd233209(VS.100).aspx。如果您指的是LINQ to SQL-即在运行时分析语法树及其转换为其他内容,则F# PowerPack支持此功能。 - Pavel Minaev
显示剩余5条评论

12

F# 相对于 C# 最大的不同在于默认情况下 F# 是不可变的。虽然在 C# 中可以复制所有不可变结构和集合,但需要付出很多额外的努力。而在 F# 中,你只需要轻松获得。

F# 是混合语言并支持一些形式的可变性。最简单的是 mutable 关键字。

let value1 = 42; // immutable
let mutable value2 = value1; // mutable
value2 <- 13;  

除了反射技巧,一旦数据在F#中创建,无论您从哪种语言使用它,它都是不可变的。


5
正如其他人所指出的,使用不可变数据有一些重要的好处。以下是其中一些:
- 并行化 - 更容易将计算结果并行化,并在不可变数据结构中返回。 - 理解代码 - 如果您使用不可变数据编写程序,则一段代码可以做的唯一事情就是返回结果。这使得程序更易读,因为通过分析输入和输出,您可以看到函数/方法的作用。
从技术上讲,您可以同时在C#和F#中编写相同的不可变数据结构。问题在于,F#鼓励您这样做,而在C#中编写真正的不可变数据结构有点痛苦。这意味着在编写F#代码时,您更有可能编写不可变结构(因为它更容易!),并且只有在真正需要时才会使用可变性(在F#中也是可能的)(更难!)
除了默认声明不可变类型外,F#还具有一些很好的功能,使得使用它们更加容易。例如,在处理记录时:
type MyRect = { X : int; Y : int; Width : int; Height : int }
let r = { X = 0; Y = 0; Width = 200; Height = 100 }

// You'll often need to create clone with one field changed:
let rMoved1 = { X = 123; Y = r.Y; Width = r.Width; Height = r.Height }

// The same thing can be written using 'with' keyword:
let rMoved2 = { r with  X = 123 }

你也可以用C#编写相同的内容,但是需要声明不可变的数据结构,并使用方法WithX返回一个带有修改后的X字段的克隆对象,以及WithYWithWidth等方法。以下是不完整示例:

class MyRect {
  readonly int x, y, width, height;

  MyRect WithX(int newX) {
    return new MyRect(newX, y, width, height);
  }
  // etc...
}

没有编译器的帮助,这是很多工作。


谢谢,那字符串值呢?我听说.NET中的字符串并不是真正的不可变。F#使用相同的字符串值吗? - Joan Venge
1
.NET字符串是真正的不可变的。所有与它们一起工作的方法都返回一个新的字符串,而不修改原始字符串(就像上面的我的WithX方法一样)。例如,'string s2 = s1.Substring(0, 4)'返回一个新的字符串,而不修改's1'。因此,F#可以安全地使用与.NET相同的字符串类型。 - Tomas Petricek

4

使用F#不可变数据相较于C#不可变数据,你能获得什么?

F#允许你轻松地创建不可变数据的修改副本,在C#中这非常繁琐且耗时。考虑在F#中创建一个记录:

type Person = { FirstName: string; MiddleName: string; LastName: string }
let me = { FirstName = "Robert"; MiddleName = "Frederick"; LastName = "Pickering" }

假设我想修改F#中的姓氏字段,可以使用“with”关键字进行复制:

let me' = { me with LastName = "Townson" }

如果您想在C#中实现这个功能,您需要创建将每个字段复制到新结构的方法,并且很可能会混淆字段。
与集合相关的其他适当优点是:例如,C#支持不可变的ReadOnlyCollection。但是,每次您想要向此集合添加新项时,您都必须复制整个集合。F#内置列表允许您仅向列表末尾添加新项,而无需复制它,即最终的结果列表实际上将共享大部分旧列表。例如:
let myList = [3; 2; 1]
let myList' =  4 :: myList

创建“myList”时,整个“myList”保持不变,并在其头部添加“4”以创建“myList'”。

谢谢。在 let myList 后面使用的是什么符号? - Joan Venge
这是一个单引号,它只是标识符名称的一部分。在 F# 中,它经常用来代替 prime 符号 http://en.wikipedia.org/wiki/Prime_(symbol),表示先前声明的标识符的另一种版本。 - Robert

3
我认为你从一个过于详细的功能对比的角度来看待这个问题。
我认为函数编程语言默认使用不可变性的主要优势在于它如何影响开发者的实际编程思维方式。当改变是默认时,编程思维方式会倾向于通过改变数据来解决问题。当不可变性是默认时,编程思维方式会倾向于通过转换数据来解决问题。
为什么后者更好?当通过改变数据来开发程序时,你最终会陷入到细节中,因此你的重点往往是如何让程序'小而精'。当通过转换数据来开发程序时,你不需要担心许多细节(例如不同的可变部分如何相互交互),因此你的重点往往是如何让程序'大而全'。因此,后者的思维方式能够让开发者在程序的工作原理方面有更好、更清晰的'全局视野'。
虽然有一个漂亮干净的程序'全局视野'是不错的,但在某些情况下,现实世界中的考虑可能会超越它。一些数据结构(如数组、多维数组)因制作新副本的高成本而不适合不可变性,因此即使在函数语言中,仍应始终应用常识来判断何时不使用不可变性。
编辑以回答评论:
函数语言纯粹主义者会认为这会使代码更丑陋。我认为自己不是一个纯粹主义者,所以我不这样认为;嗯,也许从理论角度来看会更丑陋。
F#是一种不纯的函数编程语言,因此它在需要时支持可变性。部分状态是可变的,并不会使大多数状态不可变的好处失效。
在大多数情况下,你会使用不同的(纯)数据结构,而不是在命令式语言中通常使用的数组。例如,元组、列表或树(或基于树的集/映射;标准库通常提供具有良好时间复杂度的实现)。
如果你认为你真的需要一个可变数组(出于性能原因),那么你可以使用它,所以没有什么可以阻止你使用一个可变数组。

那么,当使用数组时,如何处理诸如您所说的每次制作新副本都很昂贵的问题?这是否会使 F# 代码在这些部分变得更丑陋?任何示例都将非常有帮助。 - Joan Venge
1
Joan:通常这不会使得消耗代码变得更丑陋;它看起来只是 let newarr = mangle(oldarr)。虽然每次都制作一个新副本的成本很高,但可以设计数据结构以利用不可变性通过共享结构;例如,如果你有链表 A = (3, 2, 1) 和链表 B = (4, 3, 2, 1),并且两者都是不可变的,那么链表 B 可以由 4 和指向 A 头部的指针表示。树也可以做同样的事情。 - mqp
1
请注意,显然,如果A是可变的,你就无法使用这个技巧,因为如果你改变了A,任何持有B引用的人都会发现B也不同了。 - mqp

2
"F#不可变数据与C#不可变数据相比,有什么优势呢?第一个优点是F#函数除了返回结果以外没有其他影响。它不会改变全局数据,也不依赖于可变数据,因此只要你坚持使用不可变数据,每次调用都可以可靠地产生相同的结果。这个优点本身已经很好,因为它使得你的代码更容易理解,但真正的好处在于F#函数可以安全地组合。通过函数组合编程是一种非常强大、模块化的编程方式,可以使用可重用的部件编写更多的程序,而且可以构建非常强大的部件,这些部件可以以多种不同的方式组合在一起,因此可以用更少的代码编写更多的程序。这些想法在John Hughes的里程碑式论文Why Functional Programming Matters中得到了更好的解释。John解释了如何通过去除可变性来增加语言的表达能力。阅读这篇论文吧!"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接