Little quiz, guess what will be the result of the code below:
((string)null).GetEnumerator(); //C#
Obvious, isn’t it? And what about this?
(null : string).GetEnumerator() //F#
Expecting NullReferenceException? Well, time for little dissapointment, because this code will be compiled and executed without any errors. How can this happen, we expicitly invoke method with null instance and haven’t received any complains? Does it mean that this will be null inside the instance method? Prepare to be astonished – yes.
[<AllowNullLiteral>]
type MyClass() =
member this.IsThisNull() = (box this) = null
let b : MyClass = null
printfn "%b" (b.IsThisNull())
(*
true
*)
To find out the reason, let’s decompile result assembly and have a look to its internals:
// C# representation
public static void main@()
{
fp@1 = new PrintfFormat<FSharpFunc<bool, Unit>, TextWriter, Unit, Unit, bool>("%b");
PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<bool, Unit>>(Console.Out, Program.fp@1).Invoke(null.IsThisNull());
}
Haven’’t got new clues, maybe IL representation will be more descriptive:
.method public static void main@() cil managed
{
.entrypoint
.maxstack 4
L_0000: nop
L_0001: nop
L_0002: ldstr "%b"
L_0007: newobj instance void [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<bool, class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, bool>::.ctor(string)
L_000c: stsfld class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<bool, class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> <StartupCode$ConsoleApplication4>.$Program::fp@1
L_0011: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
L_0016: call class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<bool, class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> Program::get_fp@1()
L_001b: call !!0 [FSharp.Core]Microsoft.FSharp.Core.PrintfModule::PrintFormatLineToTextWriter<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<bool, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>(class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
L_0020: ldnull
L_0021: call instance bool Program/MyClass::IsThisNull()
L_0026: callvirt instance !1 [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<bool, class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
L_002b: pop
L_002c: ret
}
Aha, IsThisNull() is invoked with call instruction that doesn’t perform null checks for this argument. C# in many cases uses callvirt instead of call to get cheap null check. For more details please refer to the post by Eric Gunnerson (Why does C# always use callvirt). or to Eric Lippert’s comment here: Summing up: Case (1) invoke virtual method: generate callvirt. Case (2) invoke instance method on nullable receiver: generate callvirt to get cheap null check -- yes, this is typesafe. Case (3) invoke instance method on known non-nullable receiver: generate call to avoid null check. Your first example falls into category (2), your second example falls into category (3). (The compiler knows that new never returns null and therefore need not check again.).
You may think: nulls are rare in F#, we need to explicitly mark nullable types so maybe this error wouldn’t appear in my code. However F# is part of .NET platform so interoperability with other languages is mandatory feature. Lets imagine we have created (or used existing) C# library that returns some object to F# code. Factory method reports about internal errors via returning null.
public static class Factory
{
public static SomeObject Create()
{
return null;
}
}
public class SomeObject
{
public void Do(int a)
{
Console.WriteLine(a);
}
public SomeAnotherObject Create()
{
return new SomeAnotherObject(this);
}
}
public class SomeAnotherObject
{
private readonly SomeObject o;
public SomeAnotherObject(SomeObject o)
{
this.o = o;
}
public void Start()
{
o.Do(10);
}
}
// F# part
let x = Factory.Create()
let y = x.Create()
y.Start()
Compile and run our sample – it fails with NullReferenceException inside Start. Ok, in our synthetic example it is obvious – o field is null. It is initialized in constructor – find every usage of constructor. The only found case passes this as the argument and we expect this cannot be null. So what else can be the reason? Creating instance via reflection…we can spend hours searching for the real answer. Real life code can be more complex, so such corner cases look like timebomb – you can never predict when it will explode.
Интересная статья, не знал такого
ОтветитьУдалить_I think_ this has been fixed in SP1?
ОтветитьУдалитьVery interesting!
ОтветитьУдалить