Record's default implementation of GetHashCode can result in NullReferenceException
See original GitHub issueWhen using CLIMutable F# records with non-F# libraries (as in JSON or WPF scenarios), F# record fields can be null
. In this case, the default implementation of GetHashCode()
throws a NullReferenceException
. A NullReferenceException
can occur, for example, when adding the F# record to a collection or UI control that relies on GetHashCode()
.
Repro steps
> #r "Newtonsoft.Json";;
--> Referenced 'C:\Program Files (x86)\Microsoft Visual Studio 12.0\Blend\Newtonsoft.Json.dll'
> open System.Collections.Generic;;
> open Newtonsoft.Json;;
> [<CLIMutable>] type MyRecord = { Name : string; Friends : string list };;
type MyRecord =
{Name: string;
Friends: string list;}
> let json = "{ \"Name\": \"Wally\" }";;
val json : string = "{ "Name": "Wally" }"
> let me = JsonConvert.DeserializeObject<MyRecord>(json);;
val me : MyRecord = {Name = "Wally";
Friends = null;}
> let people = HashSet([ me ]);;
System.NullReferenceException: Object reference not set to an instance of an object.
at FSI_0004.MyRecord.GetHashCode(IEqualityComparer comp)
at FSI_0004.MyRecord.GetHashCode()
at System.Collections.Generic.GenericEqualityComparer`1.GetHashCode(T obj)
at System.Collections.Generic.HashSet`1.InternalGetHashCode(T item)
> at System.Collections.Generic.HashSet`1.AddIfNotPresent(T value)
at System.Collections.Generic.HashSet`1.UnionWith(IEnumerable`1 other)
at System.Collections.Generic.HashSet`1..ctor(IEnumerable`1 collection, IEqualityComparer`1 comparer)
at <StartupCode$FSI_0007>.$FSI_0007.main@()
Stopped due to error
Expected behavior
As stated in the Notes to Inheritors of the Object.GetHashCode documentation:
- The
GetHashCode
method should not throw exceptions.
Actual behavior
Here is the ILSpy-generated view of the MyRecord
GetHashCode
method. Notice that the code performs a null check on Name : string
. However, it does not perform a null check on Friends : string list
.
[CompilerGenerated]
public sealed override int GetHashCode(IEqualityComparer comp)
{
if (this != null)
{
int num = 0;
num = -1640531527 + (this.Friends@.GetHashCode(comp) + ((num << 6) + (num >> 2)));
int arg_50_0 = -1640531527;
string name@ = this.Name@;
string text = name@;
return arg_50_0 + (((text == null) ? 0 : text.GetHashCode()) + ((num << 6) + (num >> 2)));
}
return 0;
}
Known workarounds
For the JSON example, when deserializing we should check for nulls and create a new record before using it.
let replaceNulls (r : MyRecord) =
if Object.ReferenceEquals(r.Friends, null)
then { r with Friends = List.empty }
else r
However, that may not be feasible with all non-F# libraries. For example, the “real project” that motivated this bug report involves WPF binding. It’s not as straightforward to always replace null references
Related information
Provide any related information
- Microsoft ® F# Interactive version 14.0.23413.0
Issue Analytics
- State:
- Created 7 years ago
- Reactions:1
- Comments:8 (6 by maintainers)
Top GitHub Comments
@0x53A Thank you for going through the effort to create a simple WPF example!
@dsyme I can understand the argument that an F# List or Record is not supposed to be null, so GetHashCode is correct to throw a NullReferenceException. But it seems to me that the non-null constraints are more of a language feature. In contrast, GetHashCode seems like more of a .NET, language-agnostic thing that should work properly regardless. By “work properly”, I mean not throw an exception if a class instance variable (like an F# List or F# Record) happens to be null.
I think that if we leave it this way, it will fall into the category of a “gotcha” that you must warn people about when using F# with non-F# libraries.
I think this is an issue if you don’t control the creation of the object instance.
Take for example this sample project: https://github.com/0x53A/WpfFS It has two records and a datagrid which binds to an ObservableCollection<Record>.
I set
CanUserAddRows="True"
so WPF adds a pseudo-row at the end where the user can add a new Row.If you double-click on any cell, the App crashes with a null-reference exception, because WPF creates a new object with the initial values (null).
So basically your guidance is to never use records for a ViewModel, serialization or anything where the objects might temporarily be in an invalid state?
I’m probably missing something, but shouldn’t it be possible to change this backwards-compatible? If a value is null, it could just be replaced by any hashcode (e.g. 1234). If not, .GetHashCode() is called. So any valid object continues to return the same hashcode, but invalid objects return some other hashcode instead of crashing.