Рассмотрим:
> Unchecked.defaultof<list<int>>;;
val it : int list = null
> 2 :: 1 :: Unchecked.defaultof<list<int>>;;
val it : int list = [2]
Каковы причины такого поведения? Я ожидаю, что окончательный список будет [2; 1] или вызовет исключение.
Рассмотрим:
> Unchecked.defaultof<list<int>>;;
val it : int list = null
> 2 :: 1 :: Unchecked.defaultof<list<int>>;;
val it : int list = [2]
Каковы причины такого поведения? Я ожидаю, что окончательный список будет [2; 1] или вызовет исключение.
Это довольно изумительный вопрос. Я посмотрел, как списки F # скомпилированы и объясняет поведение. Я бы не счел это ошибкой (поэтому defaultof
находится в модуле Unchecked
, он может также сжечь ваш компьютер!)
Прежде всего, пустой список []
не представлен как null
в F # 2.0, потому что тогда вы не сможете вызывать методы в пустом списке - и это может сломать многие вещи (например, передать пустой список любая функция Seq
). В стороне, значение None
представлено как null
для эффективности, что означает, что option<'a>
не может реализовать интерфейс seq<'a>
, хотя это имеет смысл.
Поэтому вместо этого F # использует специальное значение list.Empty
для представления пустых списков. Теперь это значение является экземпляром того же типа, что и значения, представляющие непустой список (тип FSharpList<'T>
). Единственное отличие состоит в том, что оба поля head
и tail
экземпляра null
.
Теперь вы можете видеть, где это происходит - пустой список по существу компилируется для объекта, где поле this.tail
равно null
(и поле this.head
равно null
), но это не это важно).
Свойство list.Tail
генерирует исключение, когда поле this.tail
равно null
(потому что это означает, что вы получаете хвост пустого списка - это ошибка), но есть также внутреннее свойство list.TailOrNull
, который возвращает значение без проверки и которое используется для компиляции соответствия шаблонов.
Например, вот простая функция iter
:
let rec iter (xs:int list) =
match list with
| x::xs -> Console.WriteLine(x); iter xs
| [] -> ()
Это скомпилировано на следующий код С#:
if (list.TailOrNull != null) {
Console.WriteLine(list.HeadOrDefault);
iter(list.TailOrNull);
}
Это проверяет, является ли list
пустым списком, проверяя, является ли его list.TailOrNull
null
. Если это так, то он знает, что текущее значение list
представляет собой значение list.Empty
, представляющее пустой список (и он не печатает его HeadOrDefault
, потому что он предполагает, что это тоже null
).
Если вы используете defaultof
для создания значения null
явно, то 42::null
предоставляет объект, где TailOrNull
есть null
, а HeadOrDefault
- 42
. Поэтому значение не печатается.
TL; DR. Не используйте Unchecked.defaultof
для типов F #. Вы никогда не знаете, что произойдет.