Consider:
> Unchecked.defaultof<list<int>>;;
val it : int list = null
> 2 :: 1 :: Unchecked.defaultof<list<int>>;;
val it : int list = [2]
What's the reasoning behind this behavior? I would expect the final list to be [2;1] or raise an exception.
This is pretty amazing question. I looked how F# lists are compiled and it explains the behaviour. I would not consider this as bug (that's why defaultof
is in the Unchecked
module, it might as well burn down your computer!)
First of all, the empty list []
is not represented as null
in F# 2.0, because then you would not be able to invoke methods on empty list - and this would break many things (like passing empty list to any Seq
function). As an aside, the None
value is represented as null
for efficiency, which means that option<'a>
cannot implement the seq<'a>
interface even though it would make sense.
So instead, F# uses a special value list.Empty
to represent empty lists. Now this value is an instance of the same type as values representing non-empty list (the type is FSharpList<'T>
). The only difference is that both head
and tail
fields of the instance are null
.
Now, you can maybe see where this is going - an empty list is essentially compiled to an object where the this.tail
field is null
(and the this.head
field is null
too, but this is not that important).
The list.Tail
property will throw an exception when the field this.tail
is null
(because this means you're getting the tail of an empty list - which is an error), but there is also an internal property list.TailOrNull
which returns the value without checking and which is used to compile pattern matching.
For example, here is a simple iter
function:
let rec iter (xs:int list) =
match list with
| x::xs -> Console.WriteLine(x); iter xs
| [] -> ()
This is compiled to the following C# code:
if (list.TailOrNull != null) {
Console.WriteLine(list.HeadOrDefault);
iter(list.TailOrNull);
}
This checks whether list
is an empty list by checking whether its list.TailOrNull
is null
. If it is then it knows that the current list
value is the list.Empty
value representing an empty list (and it does not print its HeadOrDefault
because it assumes that this is null
too).
If you use defaultof
to create null
value explicitly then 42::null
gives you an object where TailOrNull
is null
but HeadOrDefault
is 42
. So that's why the value is not printed.
TL;DR - Do not use Unchecked.defaultof
with F# types. You never know what will happen.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With