F# boxing and unboxing
last modified May 3, 2025
In this tutorial, we will delve into boxing and unboxing in F# and their impact on performance and memory management.
Boxing and unboxing are mechanisms for converting between value types (e.g., integers, floats) and reference types (objects). Boxing encapsulates a value type within an object, allowing it to be treated as a reference type. Conversely, unboxing retrieves the original value type from the object.
While these operations enable flexibility when working with heterogeneous data, they can introduce performance overhead due to additional memory allocation and type conversion. Understanding how and when to use boxing and unboxing efficiently is essential for writing optimized F# code.
The box
keyword is used to box a value type, while the
unbox
keyword is used to unbox a reference type back to its
original value type.
F# boxing example
Boxing converts a value type to System.Object
.
let x = 42 let boxed = box x printfn $"Value: {x}, Type: {x.GetType()}" printfn $"Boxed: {boxed}, Type: {boxed.GetType()}"
We box an integer value and examine its type before and after.
let boxed = box x
The box
keyword converts the value type to System.Object
.
λ dotnet fsi boxing.fsx Value: 42, Type: System.Int32 Boxed: 42, Type: System.Int32
F# unboxing example
Unboxing converts System.Object
back to a value type.
let boxed = box 42 let unboxed : int = unbox boxed printfn $"Boxed: {boxed}, Type: {boxed.GetType()}" printfn $"Unboxed: {unboxed}, Type: {unboxed.GetType()}"
We unbox an integer value and verify its type.
let unboxed : int = unbox boxed
The unbox
keyword extracts the value with type annotation.
λ dotnet fsi unboxing.fsx Boxed: 42, Type: System.Int32 Unboxed: 42, Type: System.Int32
F# invalid unboxing
Unboxing to the wrong type causes runtime errors.
let boxed = box 42 try let unboxed : string = unbox boxed printfn $"{unboxed}" with | :? System.InvalidCastException as ex -> printfn $"Error: {ex.Message}"
Attempting to unbox to an incompatible type throws an exception.
let unboxed : string = unbox boxed
This fails because the boxed value is an int, not a string.
λ dotnet fsi invalid.fsx Error: Unable to cast object of type 'System.Int32' to type 'System.String'.
F# boxing performance
Boxing has performance costs due to heap allocation.
#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"
In the first test, we box and unbox a value type in a loop. The second test performs the same operation without boxing. The difference in performance is significant.
let boxed = box i
Each boxing operation allocates memory on the heap.
λ 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# avoiding boxing
Use generics to avoid unnecessary boxing.
let printValue (x: 'a) = printfn $"Value: {x}, Type: {typeof<'a>}" printValue 42 printValue "hello" printValue true
Generic functions handle value types without boxing.
let printValue (x: 'a) =
The generic parameter avoids boxing by working with the actual type.
λ dotnet fsi generic.fsx Value: 42, Type: System.Int32 Value: hello, Type: System.String Value: true, Type: System.Boolean
Boxing and unboxing are fundamental but costly operations in F#. Use them judiciously and prefer generics when possible to maintain performance.