ZetCode

F# 装箱与拆箱

最后修改于 2025 年 5 月 3 日

在本教程中,我们将深入探讨 F# 中的装箱与拆箱,以及它们对性能和内存管理的影响。

装箱与拆箱是将值类型(例如,整数、浮点数)与引用类型(对象)进行转换的机制。装箱将值类型封装在对象中,使其能够被视为引用类型。反之,拆箱则从对象中检索原始值类型。

虽然这些操作在处理异构数据时提供了灵活性,但由于额外的内存分配和类型转换,它们可能会引入性能开销。理解如何以及何时有效地使用装箱与拆箱对于编写优化的 F# 代码至关重要。

box 关键字用于装箱值类型,而 unbox 关键字用于将引用类型拆箱回其原始值类型。

F# 装箱示例

装箱将值类型转换为 System.Object

boxing.fsx
let x = 42
let boxed = box x

printfn $"Value: {x}, Type: {x.GetType()}"
printfn $"Boxed: {boxed}, Type: {boxed.GetType()}"

我们装箱一个整数值,并在装箱前后检查其类型。

let boxed = box x

box 关键字将值类型转换为 System.Object

λ dotnet fsi boxing.fsx
Value: 42, Type: System.Int32
Boxed: 42, Type: System.Int32

F# 拆箱示例

拆箱将 System.Object 转换回值类型。

unboxing.fsx
let boxed = box 42
let unboxed : int = unbox boxed

printfn $"Boxed: {boxed}, Type: {boxed.GetType()}"
printfn $"Unboxed: {unboxed}, Type: {unboxed.GetType()}"

我们拆箱一个整数值并验证其类型。

let unboxed : int = unbox boxed

unbox 关键字会提取带有类型注解的值。

λ dotnet fsi unboxing.fsx
Boxed: 42, Type: System.Int32
Unboxed: 42, Type: System.Int32

F# 无效拆箱

拆箱到错误的类型会导致运行时错误。

invalid.fsx
let boxed = box 42

try
    let unboxed : string = unbox boxed
    printfn $"{unboxed}"
with
| :? System.InvalidCastException as ex ->
    printfn $"Error: {ex.Message}"

尝试拆箱到不兼容的类型会引发异常。

let unboxed : string = unbox boxed

这会失败,因为装箱的值是 int,而不是 string。

λ dotnet fsi invalid.fsx
Error: Unable to cast object of type 'System.Int32' to type 'System.String'.

F# 装箱性能

装箱由于堆内存分配而产生性能开销。

performance.fsx
#time "on"
printfn "Testing boxing performance"

let testBoxing count =
    let mutable sum = 0L
    for i in 1L..count do
        let boxed = box i
        sum <- sum + (unbox<int64> boxed) // Ensure actual work happens

    printfn "Boxing sum: %d" sum

testBoxing 100_000_000  // Use a smaller count for practical timing

#time "off"

#time "on"
printfn "Testing no boxing performance"

let testNoBoxing count =
    let mutable sum = 0L
    for i in 1L..count do
        sum <- sum + i  // Perform equivalent work without boxing

    printfn "No boxing sum: %d" sum

testNoBoxing 100_000_000  
#time "off"

在第一个测试中,我们在循环中装箱和拆箱一个值类型。第二个测试在不进行装箱的情况下执行相同的操作。性能差异非常显著。

let boxed = box i

每次装箱操作都会在堆上分配内存。

λ dotnet fsi performance.fsx
Testing boxing performance
Boxing sum: 5000000050000000
Real: 00:00:00.902, CPU: 00:00:01.187, GC gen0: 112, gen1: 3, gen2: 2
Testing no boxing performance
No boxing sum: 5000000050000000
Real: 00:00:00.079, CPU: 00:00:00.062, GC gen0: 0, gen1: 0, gen2: 0

F# 避免装箱

使用泛型来避免不必要的装箱。

generic.fsx
let printValue (x: 'a) =
    printfn $"Value: {x}, Type: {typeof<'a>}"

printValue 42
printValue "hello"
printValue true

泛型函数在不进行装箱的情况下处理值类型。

let printValue (x: 'a) =

泛型参数通过处理实际类型来避免装箱。

λ dotnet fsi generic.fsx
Value: 42, Type: System.Int32
Value: hello, Type: System.String
Value: true, Type: System.Boolean

装箱与拆箱是 F# 中基本但代价高昂的操作。请明智地使用它们,并尽可能优先使用泛型来保持性能。

作者

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