question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Record's default implementation of GetHashCode can result in NullReferenceException

See original GitHub issue

When 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:closed
  • Created 7 years ago
  • Reactions:1
  • Comments:8 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
WallaceKellycommented, Apr 10, 2016

@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.

2reactions
0x53Acommented, Apr 10, 2016

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>.

image

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Record's default implementation of GetHashCode can ...
In this case, the default implementation of GetHashCode() throws a NullReferenceException . A NullReferenceException can occur, for example, ...
Read more >
f# - Record Equals or GetHashCode throws ...
So the issue is the compiler assumes that field3 is never null which isn't the case since DataContractJsonSerializer sets the value to null...
Read more >
Generate C# Equals and GetHashCode Method Overrides
Learn how to use the Quick Actions and Refactorings menu to generate Equals and GetHashCode methods.
Read more >
Things every C# developer should know #1: hash codes
Every C# class inherits (directly or indirectly) from Object , which exposes only 3 virtual methods.
Read more >
How to Implement Unit Tests for Equals and GetHashCode ...
First of all, Equals method tests whether object is null by using the inequality operator. But this operator indirectly relies on Equals to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found