ZetCode

F# 按值复制与按引用复制

最后修改于 2025 年 5 月 16 日

本教程通过 F# 的函数式优先方法,解释了 F# 如何处理值类型和引用类型,强调了不变性和清晰的复制语义。理解这些概念对于编写正确的 F# 代码至关重要。

默认不变性

F# 默认鼓励不变性,这简化了代码推理。

F# 区分不变类型和可变类型。不变类型按值复制,而可变类型按引用复制。这意味着赋值一个可变类型会创建一个指向原始对象的引用,而不是逻辑副本。

特征 不变类型 可变类型
默认行为 否(需要 mutable)
复制语义 按值复制(逻辑复制) 引用语义
示例 intstring、记录 mutable 变量、类、数组

在上面的表格中,我们总结了 F# 中不变类型和可变类型的主要区别。不变类型按值复制,而可变类型按引用复制。

不变值类型

F# 将原始类型视为不变值。赋值会创建逻辑副本。

Program.fs
// Primitive types are immutable
let a = 10
let b = a  // Logical copy

printfn "Original: a = %d, b = %d" a b
let b' = b + 5  // Creates new value
printfn "After change: a = %d, b' = %d" a b'

// Tuples are immutable
let tuple1 = (1, "hello")
let tuple2 = tuple1  // Copy
// tuple2.Item1 <- 2  // Would cause error

在上面的示例中,ab 都是不变整数。赋值 b = a 创建了 a 的逻辑副本。当我们修改 b 时,它不会影响 a。元组也一样,它们也是不变的。

$ dotnet fsi Program.fs
Original: a = 10, b = 10
After change: a = 10, b' = 15

记录和区分联合

F# 的记录和区分联合类型默认是不变的。它们允许进行逻辑复制和更新。

Program.fs
type Person = { Name: string; Age: int }
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float

let person1 = { Name = "Alice"; Age = 30 }
let person2 = person1  // Copy
let person3 = { person2 with Age = 31 }  // Copy with update

printfn "person1: %A" person1
printfn "person3: %A" person3

let shape1 = Circle 5.0
let shape2 = shape1  // Copy

在上面的示例中,我们定义了一个记录类型 Person 和一个区分联合类型 Shape。赋值 person2 = person1 创建了 person1 的逻辑副本。with 语法允许我们创建一个具有更新的 Age 字段的新记录,同时保持其他字段不变。

可变变量

F# 在明确请求时允许可变变量。mutable 关键字用于声明可变变量。但是,建议默认使用不变性,仅在必要时使用可变性。

Program.fs
let mutable counter = 0
counter <- counter + 1  // Mutation allowed

// Reference cells are another mutable option
let cell = ref 10
cell := 20  // Update content
printfn "Cell value: %d" !cell

在上面的示例中,我们声明了一个可变变量 counter 和一个引用单元格 cellref 类型允许我们创建对值的可变引用。:= 运算符用于更新引用单元格的内容,而 ! 运算符用于解引用它。

数组和引用类型

数组和自定义类具有引用语义。赋值复制引用。

Program.fs
let array1 = [| 1; 2; 3 |]
let array2 = array1  // Copies reference
array2.[0] <- 99     // Modifies original

printfn "array1: %A" array1
printfn "array2: %A" array2

type MutablePoint(x: int, y: int) =
    member val X = x with get, set
    member val Y = y with get, set

let p1 = MutablePoint(1, 2)
let p2 = p1  // Copies reference
p2.X <- 10   // Modifies original
printfn "p1: (%d, %d)" p1.X p1.Y

在上面的示例中,修改 array2 也会影响 array1,修改 p2 也会影响 p1。这是因为 array1p1 都是指向相同底层数据的引用。

参数传递

F# 遵循 .NET 的按值传递方法,但具有不变性重点。值类型按值传递,而引用类型按引用传递。这意味着在函数内部修改值类型不会影响原始值,但修改引用类型会。

Program.fs
let modifyValue x =
    let x' = x + 10  // Can't modify original
    printfn "Inside function: %d" x'

let modifyArray (arr: int[]) =
    arr.[0] <- 100  // Modifies original
    printfn "Inside function: %A" arr

let a = 5
modifyValue a
printfn "After modifyValue: %d" a

let nums = [| 1; 2; 3 |]
modifyArray nums
printfn "After modifyArray: %A" nums

在上面的示例中,modifyValue 不会改变原始的 a,而 modifyArray 会修改原始的 nums

复制策略

在 F# 中复制数据结构的各种方法包括浅复制、深复制和复制-更新。选择取决于数据结构类型和所需行为。

Program.fs
// Records - copy with update
let originalRecord = { Name = "Alice"; Age = 30 }
let copyRecord = { originalRecord with Age = 31 }

// Arrays - clone
let originalArray = [| 1..5 |]
let shallowCopy = Array.copy originalArray
let deepCopy = Array.map id originalArray  // Creates new array

// Lists - immutable, so "copy" is just binding
let originalList = [1; 2; 3]
let copyList = originalList  // Same list

在上面的示例中,我们演示了如何创建记录、数组和列表的副本。记录使用 with 语法进行复制和更新,而数组可以克隆或映射以创建新数组。列表是不变的,因此复制只是一个指向同一列表的引用。

总结与最佳实践

F# 处理复制和可变性的方法通过使副作用显性化,有助于编写更可预测和可维护的代码。

在本文中,我们探讨了 F# 中复制值和引用的概念。我们讨论了值类型和引用类型之间的区别、不变性的影响以及 F# 如何处理参数传递。我们还研究了各种复制策略和处理 F# 中数据结构的最佳实践。理解这些概念对于编写正确高效的 F# 代码至关重要。

来源

F# 值与不变性

F# 中的不变性

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在教授编程方面拥有超过十年的经验。

列出 所有 F# 教程