dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
288 stars 65 forks source link

Nullable References #355

Open jdmichel opened 5 years ago

jdmichel commented 5 years ago

Is there already a plan for supporting the C# 8 features related to nullable references?

What might this look like in VB?

void Foo(Bar a, Bar? b) {} -> Sub Foo(a As Bar, b As Bar?) or Sub Foo(a As Bar, Optional b As Bar) or even something else?

Even if you decided on the (breaking change?) Optional keyword then I suppose the IDE could support automatic conversion of the first syntax to save keystrokes.

There are bazillion details and corner cases, but I didn't see an existing issue on this topic, and I'm curious.

reduckted commented 5 years ago
Sub Foo(a As Bar, b As Bar?)

That would make the most sense, and aligns with the syntax for Nullable(Of T).

jdmichel commented 5 years ago

I don't think that we need nearly the complicated implementation that the C# team chose. I would instead just trust the type annotation. If it says it's nullable, then just assume it might be null. Introducing Nullable Reference Types in C#

sub M(ns as string?) ' ns is nullable
    WriteLine(ns.Length) ' Warning A: null reference
    if ns isnot nothing then
        WriteLine(ns.Length) ' Warning A: null reference
        dim s as string = ns ' Warning B: nullable to non-nullable
        WriteLine(s.Length) ' ok, not null here

        dim s2 = if(ns, "") ' If operator smart enough to infer non-nullable?
        WriteLine(s2.Length) ' ok, not null here

        if ns is nothing then
            return ' Unlike c#, this didn't buy us anything. ns is still nullable
        end if
        WriteLine(ns.Length) ' Warning A: null reference
    end if 
end sub

sub N(s as string) 
    if s is nothing then ' Warning C: 's' not nullable
    end if

    s?.Foo() ' Warning C: 's' not nullable

    s.Foo() 

    s = nothing ' Warning B: nullable to non-nullable
end sub

sub Test() 
    dim ns as string? = "Test"
    dim s as string = "Test2"
    M(ns) ' nullable to nullable is no prob
    M(s) ' Warning D: non-nullable to nullable. 
    N(ns) ' Warning B: nullable to non-nullable
    N(s) ' Fine
    N(if(ns, "")) ' If operator smart enough to infer non-nullable?
end sub

class Person 
    public FirstName as string
    public MiddleName as string?
    public LastName as string
end class

structure PersonHandle
    public person as Person' Warning E
end structure

sub Test2(p as Person)
    p.FirstName = nothing ' Warning B
    p.LastName = p.MiddleName ' Warning B
    dim s as string = nothing ' Warning B
    dim a(9) as string ' Warning F
end sub

I think it would be nice to make the warnings above separate so that the project can choose to ignore some of them. Warning E and F above were called out as special cases that C# chose to ignore. In both cases there is no way for the compiler to infer a default value for the non-nullable type.

I think this gives us the easiest possible path to this feature, while retaining 99% of the benefit of preventing most null reference exceptions.

KathleenDollard commented 5 years ago

Here are my current thoughts on this...

C# nullable reference types is a wild experiment. It's an attempt to make people rethink the way they work with and think about every single usage of reference types. Yes, there is significant compiler support. But that compiler support is to create a bazillion warnings.

It is not just the declared type. If you look at the fourth line of @jdmichel code, a warning is clearly not needed. Humans and the flow control graph (CFG) both know this cannot be null. You would not want flow control ignored, because then the code you write today to correctly manage nulls would not work. With flow control, an null guard (assert) will prove that the value cannot be null, regardless of what was known before the null guard. In the method M in my quick review, nothing should throw a warning except the first line. Otherwise, it's already protected code. IOW, you would get so many unimportant warnings in good code that the important ones would get lost.

As far as VB, we need to first see this grand experiment get traction in C#. Then we need to look at the question more deeply as to whether this makes sense in VB.NET. A feature whose purpose it to break code and force you to make significant changes to use it does not feel very VB-like - and that is thinking only of the Option Strict/Option Explicit approach. It really seems to make no sense outside that.

Someone I respect was quite surprised that I am hesitant to think this feature belongs in VB. Then they sat through a single design meeting (there have been many, many) and looked at me and said "OK, I get it"

I can't say I'm at a final decision on this. But at the current point I think the major special thing about VB is that the code does what it says it is doing - you can see and quickly comprehend that. Part of that is syntax, and maybe we could find good syntax. But part is also decades of experience that reference types are nullable and that string is nullable. Making string not null in code that has a particular gesture somewhere else in the code base, and still nullable where this gesture doesn't exist - that feels chaotic to me.

zspitz commented 5 years ago

@jdmichel

To add to @KathleenDollard 's comment (It's not just the declared type...), if we don't allow flow control to affect the understood type, the only way to use this without a warning would be to declare a new variable (Dim s As String = ns), which would require at a minimum some kind of type cast (Dim s = CType(ns, String)) in order to avoid a warning. This is not a good solution -- it would put nullable reference types in the same situation as type-checking today:

Dim o As Object
' fill o
If TypeOf o Is Random Then
    ' we cannot treat o as an instance of Random
    Dim rnd As Random = o
    ' we have to go through rnd
    ' alternatively, we could use CType
End If

Not having to do this is the "killer app" of pattern matching for VB.NET -- even newcomers to pattern matching immediately "get" this benefit. There is also a long-standing suggestion to rely on flow control within type-check blocks (#172), in order to avoid this.

jdmichel commented 5 years ago

First, thank you for responding to my suggestion.

My perspective comes from ~15 years spent writing C++ in a style that prefers use of references over pointers in 99% of all code. So I'm used to thinking of "reference" as something that cannot be null, and it always felt like Java (then C# and VB.NET) re-approriated the term reference for a concept that feels more like a pointer.

In any case, one of the great benefits of what we called "modern C++" back in the 90's was that this use of references made it easier to reason about code, because you were free to assume that such code could never contain null pointers. It decreased the cognitive load of all of our code, and meant that in practice the only time we had to deal with null was when interfacing with older C-style code from third party libraries. We wrote simplified wrappers around such code, and this is the only place you would see "if (foo_opt != null) { Foo& foo = *foo; ...}" guards to translate between the two styles. I think the C# team is going to already handle annotating most dotnet libraries with nullable type information, so I would expect these types of checks to be mostly unnecessary in VB/C# for teams that choose to embrace defaulting to non-null. Therefore, I expect it to be very rare to ever need flow control analysis for nullability. Which is why I thought VB could take a chance on embracing the non-nullable option (Option Explicit Nulls?). Although it might be nice to support flow control analysis to infer non-nullable (shadow variables?), my point is that you get 90% of the benefit without it, because in practice your code is not going to be asserting or checking for null at all, because within the projects that embrace this style then all the variables/parameters are already going to be non-nullable, and it will be extremely rare for any code to make use of nullable reference types at all. For existing code, or for people who don't want to embrace this new style, then everything continues to work as it does now.

Incidentally, I also don't understand #172, because I don't understand why I would ever have a variable/parameter of type Object that I then check for the type. In my style of VB coding (Option Explict, Option Strict) you would just rarely/never find yourself in this situation in the first place. Almost all my code would have strong types, and it isn't much trouble to wrap any code that is not type safe.

For the places that do pass nullable, I think it could be nice to make it easy to cast away nullability.

sub DoSomething(foo as Foo?) 
    if foo is nothing then
        ' handle the case where it's null
        ...
    else
       dim f = foo! ' Use exclamation to cast away the nullability to avoid the need for flow control
       ... ' Use f instead of foo from here on out because any use of foo. is now a warning (or even an error)
    end if
end sub

The same goes for dynamic types:

sub DoStuff(obj as Object) 
    if typeof obj is Foo then
       dim foo = directcast(obj, Foo)
       ' Use foo instead of obj
    else if typeof obj is Bar then
    else
    end if
end sub

That being said, thinking back, I don't think either of the above code was used much at all, because we would just never write code that took a Foo? or Object in the first place. That sort of thing might only exist at the edges where we called other peoples code, and those also usually required extreme testing for things like throwing unexpected exceptions or otherwise behaving in undocumented or incorrect ways. Or they would have complex interfaces to handle flexibliity that we didn't want or need, so we would wrap them with our own simpler type-safe non-nullable types.

Anyway, the point of all of this is to hopefully eliminate what is in my experience the most common class of bugs in Java/dotnet code, and even more importantly to make code easier to reason about by eliminating the need to consider null in most of our code. I spent 8 years working on a large Java program, and NullReferenceExceptions were extremely common, and I think embracing non-nullable defaults style would have prevented most of those bugs. It seems like the c# team was finding the same thing as they refactored existing dotnet libraries in this style. I just think they wasted alot of effort by supporting flow analysis.

zspitz commented 5 years ago

@KathleenDollard

the major special thing about VB is that the code does what it says it is doing - you can see and quickly comprehend that.

Today, when I define a variable of String in my code, I am actually saying "this should be a String, in which case I have all the behaviors of a String; but it might be this other thing which in no way behaves like a String, and any attempt to use it like a String is liable to fail". That seems a rather large underlying concept that's not immediately obvious from the code.

As you've noted, "decades of experience that reference types are nullable and that string is nullable" have firmly entrenched this concept into the heads of every .NET developer from day one; but it's certainly not something obvious from a casual reading of the code.

pricerc commented 5 years ago

String is annoying. It's a reference type that likes to pretend it's a value type (e.g. a reference type shouldn't need to be immutable). And then there's dealing with whether Empty needs to be treated the same as Nothing. And then in VB, we allow people to pretend they're not even strings and do things like math(s) on them. I'm in favour of e.g. removing + as a concatenation operator.

Back on topic: I do, however, kind of like the idea of decorating method parameters so that code analysis could know whether Nothing is a reasonable value to pass in, and post warnings about possible causes for concern.

In the discussion on the linked article, a comment is made by Mads that guard checks for null are probably still a good idea for public APIs, since external code still pass a null in and so you'd still need to be able to deal with them.

KathleenDollard commented 5 years ago

@jdmichel I think if we were designing the behavior for new language we would not have null as a default.

It is an interesting opinion that we rely only on the declared nullability and not flow in Visual Basic.

I still have concern about changing. the meaning of code - the foundational change that "Option Null Strict" or similar would bring to code.

However, you've added a layer to my thinking which is to not change the default, but allow declaration of a non-null, including in parameters. Programmers would have to do more work to track values through, including the examples you gave above. Thus there would have to be "cast-like" thing as well as a declaration site gesture. One (knee-jerk, not well thought out) idea is something that looked like pattern matching (and may or may not actually be).

As an example: this code from here bothers me. I don't yet see how you accomplish this without full safety and no warnings without expanding your suggestion. Here's a copy of the part that troubles me:

if ns isnot nothing then
        WriteLine(ns.Length) ' Warning A: null reference
        dim s as string = ns ' Warning B: nullable to non-nullable
        WriteLine(s.Length) ' ok, not null here

As a possible solution, I'll rewrite temporarily using ! as the not-null specifier and a version of pattern matching syntax (which we are certainly not decided on).

Dim ns As String
WriteLine(ns.Length) ' No warning, this is oblivious as today
If ns Is String! Into s
      WriteLine(s.Length) ' No warning, it's for real non-null
End If

' For intentional nullable
Dim nn As String?
WriteLine(nn.Length) ' Warning, it can definitely contain null

In that direction, Option Null Specified (or similar) would simply outlaw the first line of my sample.

Code would always do the same thing.

C# did not go the route of specifying everything, because they felt it would be too much explicitness - but VB thrives on explicitness. I actually think if we did this I'd like to fall even deeper into the explicitness to:

Dim ns As String
WriteLine(ns.Length) ' No warning, this is oblivious as today
If ns Is NonNull String Into s
      WriteLine(s.Length) ' No warning, it's for real non-null
End If

' For intentional nullable
Dim nn As Nullable String

Anyway, these are just my top of the head thoughts on this.

pricerc commented 5 years ago

Slightly random thought

While I get where they're coming from, the Nullable Reference type being introduced in C# is not quite the same as a Nullable Value type, because Nullable Value types are built on Nullable(Of T).

To be consistent, Nullable(Of T) could be extended to allow T to be a Class or a Structure. Then the 'Value' property may be Nothing or an instance of T, and for a Class, HasValue just returns Value IsNot Nothing

Then


Public Sub GetCask(value as Wine)
    If Wine Is Nothing Then Throw New ArgumentNullException
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

could become


Public Sub GetCask(value as Wine?)
    If Not Wine.HasValue Then Throw New ArgumentNullException
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

And at the same time decorating the method with the intention to allow nulls.

zspitz commented 5 years ago

@KathleenDollard

if we did this I'd like to fall even deeper into the explicitness

Would such a syntax (Nullable / NonNullable) be extended to value types, leaving two ways to specify nullable value types?

Is value type? Intentionally can contain Nothing Cannot contain Nothing Ambiguous about Nothing
Value type Integer? Integer
Value type Nullable Integer Integer
(NonNull Integer would be invalid)
Reference type Nullable String NonNull String String

Or would there be two separate syntaxes for value types vs reference types:

Is value type? Intentionally can contain Nothing Cannot contain Nothing Ambiguous about Nothing
Value type Integer? Integer
Reference type Nullable String NonNull String String

Either possibility would add cognitive load to code authors.

And if we're stuck with using symbols for explicitly-nullable- and non-nullable-reference-types, then the VB.NET explicitness argument falls down. VB.NET is easy to read and understand because the only symbols used in the language are almost universally understood even from a non-programming perspective -- e.g. mathematical operators (+, -, *, /), comparison operators (<, >, =) or order of operations ((,)). Virtually everything else is represented with keywords. But I think a design decision that would result in more symbols runs counter to readability -- APL with its' multitude of symbols may be explicit, but is less readable, because you have to know what the symbols mean.

I still have concern about changing. the meaning of code - the foundational change that "Option Null Strict" or similar would bring to code.

Having used Typescript both before and after undefined and null were isolated into their own types, I can attest that it took me about two weeks to make the mental transition between "the string type obviously includes undefined or null as one of its possible values" to "why in the world should the string type treat undefined / null as a possible value?"

I would suggest that it's not such a foundational change. Even today, Nothing has little value when typed by the compiler as String -- you can't read any properties of a normal String, call any of it's methods; if you try to pass it into another method, you're liable to get an ArgumentNullException. Virtually the only thing you can do, is filter out the possibility that the String might be Nothing before attempting to use it as a regular String.

jdmichel commented 5 years ago

I think this is a great discussion, and I hope we're closing in on a solution that everyone will like. I have several thoughts after re-reading and thinking about the above.

  1. I think it's completely in the character of VB to have an option that changes the meaning and behavior of existing code. Option Strict, Compare, and maybe even Explicit all do this. So it still seems to me that something like Option NonNull On that changed the default for all reference types to prevent null would be easy to understand. I even think this could make sense as errors instead of warnings, because you are still opting in to the behavior. I wish this were easy to hack in to VB so that I could try to refactor my largest VB project to try it out. I've been mostly thinking of it from the perspective of something I'd only use for new code, but maybe it's more realistic to think of it from the perspective of VB teams deciding to adopt it for existing code. My gut says it would be similar, but easier, than enabling Option Strict On and Option Explicit On for an existing project.

  2. I haven't been following proposals for pattern matching. (Is there a better name for this? I find it confusing.) It looks like a replacement for TryCast to me, so maybe it would be less confusing as:

    if trycast ns as string into s then
    WriteLine(s.Length)
    end if

    (Btw, a few years ago I started using Anthony Green's April Fools font to let me try VB with all lowercase keywords, and I've grown to greatly prefer that look. I always typed VB keywords as lowercase then relied on the IDE to fix the case, but I find it's enough feedback for the IDE to colorize keywords so that I know it understood me.)

Maybe we could even support a default for the 'as' clause to cast away the nullability so that the following would be a more concise way to do the same thing:

if trycast ns into s then
end if

Or maybe it's more consistent if you rearrange the first syntax to:

if trycast ns into s as string then...

That way it's clear that the 'as string' part can be inferred if Option Infer On is set.

  1. I like keywords over special symbols, but have to admit that in one project that used nullable value types I found it much clearer to just use question marks. But maybe we don't actually need the exclamation point at all if we have something like pattern matching (still don't like that term) above?

  2. I think Option NonNull(able?) On makes reference types more consistent with value types, and therefore easier to understand. To ensure symmetry you would also want to support the pattern matching syntax for value types.

    dim apples as Nullable(of Integer) = 42
    if trycast apples into count as integer then
    SendApples(count)
    end if

    This seems more readable for both value and reference types, but with Option NonNull Off you could still support the following without any new keyword (just an implicit new name for the new kind of non-nullable reference):

    dim ns as Nullable(of string) 
    if trycast ns into s as NonNullable(of string) then
    Console.WriteLine(s.Length)
    end if

    Turning on Option NonNull(able) would then simply bring the defaults for references in line with the defaults for value types and let you simplify the above syntax to:

    dim ns as string?
    if trycast ns into s as string then
    Console.WriteLine(s.Length)
    end if

    This feels very much in the spirit of VB to me, but maybe I'm just too close to it, and it's been a pet peeve of mine since I first used VB (and others like Java, C#, Python, JavaScript), because I was already used to C++ non-nullable references.

zspitz commented 5 years ago

@jdmichel

I find it (pattern matching) confusing.

My mental model of pattern matching looks something like this: image You define a pattern, or set of patterns, and the object in question is tested against each pattern if it matches. Pattern syntax could theoretically describe a specific type:

Dim o As Object
Select Case o
    Case String: Console.WriteLine("It's a string")
    Case Integer: Console.WriteLine("It's an integer")
End Select

or something else:

Dim o As Object
Select Case o
    Case 5 To 10: Console.WriteLine("It's a number or numeric string between 5 and 10")
    Case Like "A*B": Console.WriteLine("It's a string that starts with A and ends with B")
End Select

An additional goal of pattern matching is to extract all or part of the object into new variables:

Dim o As Object
Select Case o
    Case String Into s:  Console.WriteLine($"Length of string: {s.Length}")
    Case Integer Into i: Console.WriteLine($"i ^ 2 = {i^2}")
    Case With {.LastName = "Smith", .DOB Matches > #1/1/1985# Into Since1985}
        Console.WriteLine("Much more concise than the following alternative, with only a single new variable")
        Console.WriteLine("Also works if the object is not of the known type Person")
        ' If TypeOf o Is Person Then
        '     Dim p As Person = o
        '     If p.LastName = "Smith" AndAlso p.DOB > #1/1/1985# Then
        '         Dim Since1985 = p.DOB
        '     End If
        ' End If
    Case (String Into s, Integer Into i)
        Console.WriteLine($"Tuple of string '{s}' and integer '{i}'")
End Select

It's rather more than just a replacement for the TryCast-into-a-variable idiom, because patterns could theoretically represent much more than just a type + variable assignment.

Because pattern matching is a generalized syntax for matching patterns, and not just typechecking+variable assignment, I don't think it appropriate to modify the pattern matching syntax just for this use case.

pricerc commented 5 years ago

Although the discussion is nominally about nullable reference types; Is that not really a misnomer, since reference types are by definition nullable?

It looks to me more that we're talking about the potential of 'Not Nullable' reference types as additional tool in our toolbelt.

What we're really talking about is a new semantic option (compiler flag) that allows the compiler and/or code analysis to make better predictions about potential problems (primarily) with passing reference-type arguments to Public(and maybe Friend) methods (including property setters) that don't really want null values.

So. The way I see it, what I'd like from this concept is: 1) a mechanism where the compiler generates appropriate null guards for me. 2) the compiler warns me if I'm passing Nothing to a Nothing-not-wanted parameter.

From the C# blog:

Naively, this suggests that we add two new kinds of reference types: “safely nonnullable” reference types (maybe written string!) and “safely nullable” reference types (maybe written string?) in addition to the current, unhappy reference types.

We’re not going to do that....

I don't think that logic applies to VB. I think it would be preferable to give people the option of using a new way of doing things, if it adds value to what they're doing, using exactly the constructs they're describing.

So I think adding both ! and ? decorations to reference type method parameters has merit to permit per-method, explicit declaration of intent. When introduced along with an appropriate Option.

The name of the option should describe clearly the handling of null references: 1) the 'Legacy' way, references are implicitly nullable 2) the 'New' way, references must be explicitly marked as nullable

So how about something like Option NullableReference Implicit|Explicit ? where Implicit is the current behaviour, and Explicit inverts it. For short form, maybe NullRef.

When combined with ! and ?, you can mix & match:

Option NullRef Implicit

Class Wine
    Public Property Name As String
    Public Property Vintage As Integer
End Class

' 1) NullRef Implicit; this enforces NotNullable semantics on `value`
Public Sub GetCaskExplicitNotNull(value as Wine!) 
    ' 1a) Compiler inserts null reference check here, 
    '       throws ArgumentNullException(NameOf(value)) if value is Nothing
    ' 1b) value is defined, all good
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 2) NullRef Implicit; this works the same way it always has.
Public Pub GetCaskImplicitNullA(value as Wine) 
    ' 2a) Compiler does *not* insert null reference check
    ' 2b) Seeing no user-defined reference check, compiler issues warning.
    ' 2c) Potential RunTime NullReferenceException:
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 3) NullRef Implicit; this works the same way it always has.
Public Pub GetCaskImplicitNullB(value as Wine) 
    ' 3a) Compiler does *not* insert null reference check
    ' 3b) Seeing user-defined reference check, compiler issues no warning.
    If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
    ' 3c) value is defined, all good:
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 4) NullRef Implicit; this is redundant, but still legal.
Public Sub GetCaskExplicitNullA(value as Wine?)                                           
    ' 4a) Compiler does *not* insert null reference check, 
    ' 4b) Seeing no user-defined reference check, compiler issues warning.
    ' 4c) RunTime NullReferenceException if value is null:
    Console.WriteLine($"{value.Name} {value.Vintage}")                        
End Sub

' 5) NullRef Implicit; this is redundant, but still legal.
Public Sub GetCaskExplicitNullB(value as Wine?)                                           
    ' 5a) Compiler does *not* insert null reference check.
    ' 5b) Seeing user-defined reference check, compiler issues no warning.
    If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
    ' 5c) value is defined, all good:
    Console.WriteLine($"{value.Name} {value.Vintage}")                       
End Sub

' 6) If Nullable(Of T) extended to allow T to be a reference type, then:
' NullRef Implicit; this is redundant, but still legal.
Public Sub GetCaskExplicitNullC(value as Wine?)                                           
    ' 6a) Compiler does *not* insert null reference check
    ' 6b) Nullable(Of T) lets you do this; and seeing a user-defined reference check, compiler issues no warning.
    If Not value.HasValue Then Throw New ArgumentNullException(NameOf(value))
    ' 6c) value is defined, all good:
    Console.WriteLine($"{value.Name} {value.Vintage}")                       
End Sub

Public Sub CallGetCasks
    Dim newWine As Wine = Nothing

    ' Compiler warning, RunTime ArgumentNullException from compiler-added guard code
    GetCaskExplicitNotNull(newWine) 

    ' No compiler warning, RunTime NullReferenceException (from called method)
    GetCaskImplicitNullA(newWine) 

    ' No compiler warning, RunTime ArgumentNullException (from called method)
    GetCaskImplicitNullB(newWine) 

    ' No compiler warning, RunTime NullReferenceException (from called method)
    GetCaskExplicitNullA(newWine) 

    ' No compiler warning, RunTime ArgumentNullException (from called method)
    GetCaskExplicitNullB(newWine) 

    ' No compiler warning, RunTime ArgumentNullException (from called method)
    GetCaskExplicitNullC(newWine) 
End Sub

or

Option NullRef Explicit

Class Wine
    Public Property Name As String
    Public Property Vintage As Integer
End Class

' 1) NullRef Explicit; this is redundant, but still legal
Public Sub GetCaskExplicitNotNull(value as Wine!) 
    ' 1a) Compiler inserts null reference check here,
    '       throws ArgumentNullException(NameOf(value))  if value is Nothing
    ' 1b) value is defined, all good
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 2) NullRef Explicit; value becomes 'NotNullable' 
Public Pub GetCaskImplicitNotNullA(value as Wine) 
    ' 2a) Compiler inserts null reference check here,
    '       throws ArgumentNullException(NameOf(value))  if value is Nothing
    ' 2b) value is defined, all good
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 3) NullRef Explicit; value becomes 'NotNullable' 
Public Pub GetCaskImplicitNotNullB(value as Wine) 
    ' 3a) Compiler inserts null reference check here,
    '       throws ArgumentNullException(NameOf(value))  if value is Nothing
    ' 3b) Compiler sees you have too, issues warning.
    If value Is Nothing Then Throw New ArgumentNullException(NameOf(value)) 
    ' 3c) value is defined, all good
    Console.WriteLine($"{value.Name} {value.Vintage}")
End Sub

' 4) NullRef Explicit; value is Nullable
Public Sub GetCaskExplicitNullA(value as Wine?)                                           
    ' 4a) Compiler does *not* insert null reference check, 
    ' 4b) Seeing no user-defined reference check, compiler issues warning.
    ' 4c) RunTime NullRefException if value is null:
    Console.WriteLine($"{value.Name} {value.Vintage}")                        
End Sub

' 5) NullRef Explicit; value is Nullable.
Public Sub GetCaskExplicitNullB(value as Wine?)                                           
    ' 5a) Compiler does *not* insert null reference check.
    ' 5b) Seeing user-defined reference check, compiler issues no warning.
    If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
    ' 5c) value is defined, all good:
    Console.WriteLine($"{value.Name} {value.Vintage}")                       
End Sub

' 6) If Nullable(Of T) extended to allow T to be a reference type, then:
' NullRef Explicit; value is Nullable
Public Sub GetCaskExplicitNullC(value as Wine?)                                           
    ' 6a) Compiler does *not* insert null reference check
    ' 6b) Nullable(Of T) lets you do this; and seeing a user-defined reference check, compiler issues no warning.
    If Not value.HasValue Then Throw New ArgumentNullException(NameOf(value))
    ' 6c) value is defined, all good:
    Console.WriteLine($"{value.Name} {value.Vintage}")                       
End Sub

Public Sub CallGetCasks
    Dim newWine As Wine = Nothing
    ' Compiler warning, RunTime ArgumentNullException from compiler-added guard code
    GetCaskExplicitNotNull(newWine) 

    ' Compiler warning, RunTime ArgumentNullException from compiler-added guard code
    GetCaskImplicitNotNullA(newWine) 

    ' Compiler warning, RunTime ArgumentNullException from compiler-added guard code
    GetCaskImplicitNotNullB(newWine) 

    ' No compiler warning, RunTime NullRefException (from called method)
    GetCaskExplicitNullA(newWine) 

    ' No compiler warning, RunTime ArgumentNullException (from called method)
    GetCaskExplicitNullB(newWine) 

    ' No compiler warning, RunTime ArgumentNullException (from called method)
    GetCaskExplicitNullC(newWine) 

End Sub

Assignments between nullable and not-nullable references should have the same semantics as assignments between T and Nullable(Of T) has today.

zspitz commented 5 years ago

@pricerc

Although the discussion is nominally about nullable reference types; Is that not really a misnomer, since reference types are by definition nullable?

Not quite, although it is a little confusing. The C# proposal actually consists of two basic things:

  1. Reinterpret every current usage of a reference type as excluding Nothing, so warnings can be issued on potential misuse; in other words, current reference types -> non-nullable reference types.
  2. A syntax variation is now needed to define reference types that can include Nothing; this is the syntax that is being added to the language -- a syntax for nullable reference types. Hence, the name.

I think it would be preferable to give people the option of using a new way of doing things, if it adds value to what they're doing

I would suggest that we first have to consider what the right design should have been. In VB.NET, the shape of an object as recognized by the language comes from its type, wherein are defined the methods, properties, events, constructors etc. that are relevant for objects of a given type (inheriting or implementing). But Nothing has a completely different shape -- none of the shape of the type applies to Nothing. Therefore, bundling Nothing as a valid value for reference types is a sort of violation of the contract implied by the type's shape.

It's true that the CLR type system does treat Nothing as a value compatible with reference types, but what's being proposed here is to add a refinement to the virtual type system that already exists on top of the CLR type system; akin to how Typescript is a virtual type system on top of Javascript, which has no type system in the language at all. (This also has the benefit of not being a change to the CLR, or even to IL emitted by the compiler.)

If we can agree that ideally Nothing should be excluded from the domain of a given reference type, then saddling every future usage of reference types with an explicit annotation to denote non-nullability seems inappropriate.

The only difference here between C# and VB.NET, is that C# has a large community of enthusiasts, who would be happy to accept additional warnings as the price of more correct code, and are thus willing to drive these kinds of changes; my perception is that the VB.NET community is smaller, and less users who would be willing to pay this price.

Bill-McC commented 5 years ago

I honestly don't get this. Every time I think of the edge cases I keep coming up with a cat in a box ....

At what point during instantiation is an objects fields null or not null. If they cannot be null, then all instantiation has to be to a default which is just another form null with another a value. VB already went down that road with strings and equality testing with Nothing. And the complexities of ensuring there is no inadvertent assignment of Nothing to a reference type that has to have a default value becomes daunting when considering query language. We've added constructs to deal with nested nulls, but that defaults to null anyway so would be pointless if mulls were no longer allowed. In the end, to me it seems we want to add a check for nulls that as far as possible can be picked up at compile time. I don't think we should change the nature of existing code nor introduce what seems to me to be areas of uncertainty. But like I said I honestly don't get it, I just keep seeing a cat in a box. If something at runtime has no value, what is it? Is it nothing, or is it something? This quantum stuff always does my head in ;)

jdmichel commented 5 years ago

My point about pattern matching being confusing is that VB already has several different concepts that could be called pattern matching.

  1. If x Like y Then
  2. Select Case Neither of these is suitable for the thing that I called TryCast Into, and I didn't understand why that concept would be known as pattern matching.

I googled and found Features of a pattern matching syntax, so I now understand how it relates. What do you think of:

Select Case obj
Case TryCast Into f as Foo
    f.DoFoo()
Case TryCast Into b as Bar
    b.DoBar()
End Select

So within a Select Case the variable being tested is implied, but outside of a Select you would specify it.


If TryCast obj Into f As Foo Then
    f.DoFoo()
End If
Dim isMatch = TryCast obj Into f As Foo
Dim isNotNull = TryCast maybeNull Into nn As Bar
jdmichel commented 5 years ago

I'm really confused by the whole "cat in a box" thing.

I think maybe I see what you mean if you're pointing out that it's not clear what assigning Nothing to a non-nullable reference type should do. I think many people are probably confused that Nothing does not mean Null, and instead means Default, because in practice with nullable references the default could be Null. But what is the default for a NonNullable reference?

  1. It could be Null still, which would just make any attempt to assign it an error. However, that seem non-symettrical, and feels weird to define something illegal as the default.
  2. It could be a default constructed object, but that won't work since it breaks the expectation that Nothing is a single default so you couldn't test it with If x Is Nothing
  3. We could introduce some way to provide a default for each type, but that seems complicated to understand too, and may have all sorts of follow-on consequences.

The first seems easiest to me.

Bill-McC commented 5 years ago

A reference type at one stage or another is always null; if not it would be a value type. So if we say a reference type is not nullable, we aren't actually talking about the type, rather a state the type can be in. And as much as the compiler can try to enforce it, by the very nature of the type, at some point it will be null. And the only way we can be sure is to open the box, to test for null.

pricerc commented 5 years ago

@pricerc

Although the discussion is nominally about nullable reference types; Is that not really a misnomer, since reference types are by definition nullable?

Not quite, although it is a little confusing. The C# proposal actually consists of two basic things:

1. **Reinterpret every current usage of a reference type as excluding `Nothing`**, so warnings can be issued on potential misuse; IOW, _reference types_ -> _non-nullable reference types_.

2. A syntax variation is now needed to define reference types that can include `Nothing`; this is the syntax that is being **added to the language -- a syntax for _nullable reference types_.** Hence, the name.

"tomato, tomato".

I read the blog. And I call it as I see it. They can spin it however they like, they're still talking about introducing a new concept of non-nullable reference types.

That they're wanting to gently shove C# people in that direction is fine. If C# enthusiasts are still struggling with the concept of null, then they probably need help :D.

I would suggest that we first have to consider what the right design should have been. ...

That's fine. But doing what C# is doing, which is introducing (even if opt-in) a breaking change to 20 years of legacy code, is simply not acceptable in the VB world. Which is why I would rather 'add' the feature as something that VB developers can choose to use. Or not.

... bundling Nothing as a valid value for reference types is a sort of violation of the contract implied by the type's shape.

Which is why I suggested that Nullable(Of T) should be extended to allow T to be a reference type. Which I think would then make all types, value or reference, have a common way of dealing with nullability.

...then saddling every future usage of reference types with an explicit annotation to denote non-nullability seems inappropriate.

Agreed. And being in VB-land, we'll make it an Option, and so maintain backward compatibility and allow our developers to move over at their own pace, as they deem appropriate in their SDLC.

my perception is that the VB.NET community is smaller, and less users who would be willing to pay this price.

I suspect VB developers are a little bit like many non-mainstream-thinkers in the world today - scared to raise their heads for fear of being called out for innappropriate language use, or that they'll be DOX'ed for being VB fanciers.

pricerc commented 5 years ago

To TL;DR my earlier textbook (apologies for that):

1) Add a new option. I think Option NullRef|NullableReference Explicit|Implicit describes what I'm thinking.

2) Add annotations ! and ? to reference types

3) When a 'Not-nullable' parameter is specified in a method (or property), then the compiler should add a null check and ArgumentNullException.

By having this combination, you can code with or without using the options, and whichever convention suits your purposes.

This doesn't cover everything the original concept is going for, but I think it covers most of it, relatively simply.

Notes:

jdmichel commented 5 years ago

Maybe this will help. Let's try to understand what the dotnet clr looks like behind the scenes (simplified). If you run the following VB code: Dim r = new MyObject() Then we now have several different pieces:

  1. There exists a reference type named MyObject
  2. There exists an object of that type.
  3. There exists a reference (aka pointer) to #2 named "r"

Currently lets pretend that #3 looks something like this psuedocode:

Public Class NullableReference(Of T)
    Private myT As T

    Public Sub New(val As T)
        myT = val
    End Sub

    Public Property Value As T
        Get
            Return myT
        End Get
        Set(value As T)
            myT = value
        End Set
    End Property
End Class

What I'm trying to suggest is that we change the compiler to instead implement #3 as:

Public Class ImmutableReference(Of T)
    Private ReadOnly myT As T

    Public Sub New(val As T)
        myT = val
    End Sub

    Public ReadOnly Property Value As T
        Get
            Return myT
        End Get
    End Property
End Class

And I guess now that I write it out, there is an additional missing concept we haven't discussed which is whether non-nullable references should also be immutable. If I understand the metaphor correctly, ImmutableReference ==> BoxedCat.

Bill-McC commented 5 years ago

The opposite. A reference type is mutable. It always starts as null. We can write special cases where we make it mutable only at instantiation, but that is a special case not a Type in the broad sense (as in ValueType, ReferenceType, Nullable ValueType etc). What I was trying to get at is, no matter what you call it, the type is still a reference type and it can still be nullable or not. As Mads said,

There is no guaranteed null safety, even if you react to and eliminate all the warnings. There are many holes in the analysis by necessity, and also some by choice.

So it remains a cat in the box. (dead or not dead ?)

What we are interested in is null checking and safe code flow. This is what we practice today. We move away from the unknown state to a known state: we open the box and examine the cat.

So rather than pretend it be something it isn't I think we should just focus on those aspects. You can never guarantee it cannot be null, hence there is no such thing as a non nullable reference, and all reference types are nullable .

C# seems to be taking a shorthand approach, and annotating the type declaration to indicate if the compiler will raise warnings or not. The code can be compiled, warnings ignored, and a string?, string!, or string are still System.String, still all capable of being null. It is a "leaky abstraction"

If VB wanted to improve code flow checking at compile time, I think using attributes that say "warn on null", or similar would be a better fit. And being a compiler directive attribute it could be applied at field, parameter, variable, method, class, code file, maybe even project.

KathleenDollard commented 5 years ago

@Bill-McC Are you suggesting more of an Assert strategy? With nice gestures to make it easy?

Bill-McC commented 5 years ago

@KathleenDollard not sure. I think more thorough compiler warnings/assertions, something it is easy for existing code to opt into, and something that makes it easy to opt out of at a granular level. I definitely do not like the idea of pretending a reference type can't be null. So if I want to have a way if declaring null checks, say on parameters on a method call, I don't see much value in declaring in the signature because ultimately that can and will result in a runtime null error that will be no easier to trace. So moving that into code block

Bill-McC commented 5 years ago

Moving into code block like an AssertNotNull(param, param, Action) or similar might help speed writing code.

zspitz commented 5 years ago

@Bill-McC

A reference type is mutable. It always starts as null. We can write special cases where we make it mutable only at instantiation, but that is a special case not a Type in the broad sense (as in ValueType, ReferenceType, Nullable ValueType etc). What I was trying to get at is, no matter what you call it, the type is still a reference type and it can still be nullable or not. As Mads said,

There is no guaranteed null safety, even if you react to and eliminate all the warnings. There are many holes in the analysis by necessity, and also some by choice.

So it remains a cat in the box. (dead or not dead ?)

This is all true if the type system in VB.NET must precisely reflect the CLR type system. At the level of the CLR, a reference type always starts as null, and as long as it it mutable, can be set to null.

But what if we define a virtual type layer that exists only at the language level? Those types could have additional rules enforced by the compiler. It could be argued that VB.NET already does this -- extension methods can be called as if they were instance methods, even though they don't actually exist on the extended type.

It's true that because the CLR rules would be more lax, there is a greater potential for leaks in this abstraction; but that goes back to the point Mads made in the original post -- this kind of abstraction will never be perfect.

The example of Typescript is instructive here. If you want to be technical, Javascript does have a very primitive type system -- there is a single type enforced by the compiler/interpreter, to which language-defined keywords and operators can be applied. Trying to use arbitrary keywords/operators not defined by the language will be rejected by the interpreter. All of Typescript's types are simply a virtual layer on top of the "real" type system, one based more on runtime behavior than compilation.

Bill-McC commented 5 years ago

@zspitz if only at language level it would be even more prone to failure as it would fall down at every framework call. The example of extension types, putting aside they fact they can only access non private members, they also have to deal with the instance they are referring to as being null. And this makes sense as often in LINQ the result of a query can be null. And so we have operators such as ?. simply because we can never do away with null, there is no such thing as a default customer. The goal is not to do away with null checking, rather it is to help track down ppssible null exception sources at compile time. Ironically if you were to decorate a parameter as not allowing nulls, you are actually saying that code is not doing any null handling rather it may throw null exceptions.

KathleenDollard commented 5 years ago

We won't be changing the type system for null-reference types (which, yes, is a bit of a misnomer).

It's hard to imagine that with C# picking the flow analysis route, and being quite happy with what they've achieved with it, that VB would take a route of a virtual type system.

pricerc commented 5 years ago

@ Bill-McC if

Sub SomeMethod(value as MyClass!)
    ' do something
End Sub

is translated by the compiler into

Sub SomeMethod(value as MyClass)
    If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
End Sub

Then would the runtime error not then be in the 'right place' - in the method that doesn't want nulls?

Bill-McC commented 5 years ago

@pricerc yes it would be in the right place as a null check.

reduckted commented 5 years ago

I'm a bit late to the party here, so apologies if I've already covered what others have said. 😄

@KathleenDollard But that compiler support is to create a bazillion warnings.

But C#'s implementation is opt-in. You choose if you want it to create "a bazillion warnings".

@KathleenDollard A feature whose purpose it to break code and force you to make significant changes to use it does not feel very VB-like.

😕 But it's not breaking code. And again, you opt-in to use it. You wouldn't turn this behavior on in an old codebase unless you felt like enabling it everywhere and fixing warnings for the next month. You'd either wouldn't enable it at all, enable it gradually, or only use it in new code from the outset.

@KathleenDollard IOW, you would get so many unimportant warnings in good code that the important ones would get lost.

If you're going just off what @jdmichel wrote in his example code, then yes. But there's a mistake in it - the last line should not be a warning. In the linked blog post there's an example M function that looks very similar, but is not the same (I'm not sure if it was translated incorrectly, or if it's just a coincidence that it's similar). It has two warnings, and both are perfectly valid. Here's the translation to VB:

Sub M(ns As String?)            ' ns Is nullable
    WriteLine(ns.Length)        ' WARNING: may be null

    If ns IsNot Nothing Then
        WriteLine(ns.Length);   ' ok, Not null here 
    End If

    If ns Is Nothing Then
        Return                  ' Not null after this
    End If

    WriteLine(ns.Length)        ' ok, Not null here
    ns = Nothing                ' null again!
    WriteLine(ns.Length)        ' WARNING: may be null
End Sub

There's a warning right at the start becaues ns might be null at that point. Then everything else is fine, right up to the point where ns is set to Nothing, meaning it may now be null. The final warning is where ns is used after it's been set to null. Both warnings are exactly what I would expect.

@KathleenDollard Someone I respect was quite surprised that I am hesitant to think this feature belongs in VB. Then they sat through a single design meeting (there have been many, many) and looked at me and said "OK, I get it"

Care to share some details then? I don't understand why you think this doesn't belong in VB, so it would be great to hear why you think that's the case.

@pricerc If Nullable(Of T) could be expanded to include reference types, it would make all types have consistent nullable behaviour.

That would be a (breaking?) change to the CLR, and given that C# is the king, with all the work they've done with nullable/non-nullable reference types, that change has zero chance of being done. 😄

@KathleenDollard It's hard to imagine that with C# picking the flow analysis route, and being quite happy with what they've achieved with it, that VB would take a route of a virtual type system.

This is what I'm really confused about. With what's been done in C#, assuming it takes off (and it has to, right?; it's already out in the wild, even if it's in beta, so there's really no going back at this point), why wouldn't VB just do exactly the same thing that C# has done?

zspitz commented 5 years ago

@KathleenDollard

We won't be changing the type system for null-reference types.

Isn't that precisely what C# is going to do, with this mode turned on? Until now, elements of type string could include in their set of values, the value null. But after null reference types, string has been redefined -- string-typed elements cannot include the value null (or at least the compiler will issue a warning about it).

It's hard to imagine that with C# picking the flow analysis route, and being quite happy with what they've achieved with it, that VB would take a route of a virtual type system.

Could you clarify why the two are mutually exclusive? Isn't the flow analysis deducing at a given point in the code what the appropriate subtype of CLR String is -- non-nullable String or nullable String?

zspitz commented 5 years ago

@Bill-McC If I understand you correctly, your objection to the C# implementation is because it's a leaky abstraction, Doesn't every abstraction leak somewhere? And if so, the question of whether to use the abstraction doesn't have a simple yes/no resolution; the following questions need to be answered for any abstraction -- how often does the abstraction leak vs. how often doesn't it leak? is the leak easily resolvable by dropping to a lower level, or not? does the abstraction provide little value, or a lot of value?

Are you saying that the leaks in the abstraction will be so common, and so hard to fix, that any possible value in the abstraction is canceled out?

pricerc commented 5 years ago

@pricerc If Nullable(Of T) could be expanded to include reference types, it would make all types have consistent nullable behaviour.

That would be a (breaking?) change to the CLR

I don't think so. Nullable(Of Object) should be a fairly simple variation of Nullable(Of Structure).

You can already do this (with a bit of help from StackOverflow), which (I'm pretty sure) has the right semantics :


Public Class NullableReference(Of T)
    Private _value As T
    Public Sub New(ByVal value As T)
        _value = value
    End Sub

    Public ReadOnly Property HasValue As Boolean
        Get
            Return _value IsNot Nothing
        End Get
    End Property

    Public Property Value As T
        Get
            Return _value
        End Get
        Set(value As T)
            _value = value
        End Set
    End Property

    Public Shared Widening Operator CType(ByVal value As T) As NullableReference(Of T)
        Return New NullableReference(Of T)(value)
    End Operator
    '
    Public Shared Widening Operator CType(ByVal value As NullableReference(Of T)) As T
        Return value.Value
    End Operator
End Class
Bill-McC commented 5 years ago

@pricerc wouldn't that imply a branch in an inheritance chain ?

@zspitz I think the abstraction is flawed, leads to confusion as to whether it is a decorator or type, and limits the opt in/out options to a type system rather than code blocks.

pricerc commented 5 years ago

@pricerc wouldn't that imply a branch in an inheritance chain ?

I don't know that I'm smart enough and/or know enough about VB, .NET and all their bits to answer that question.

I'm here to learn, and contribute ideas where I can about a language that I value. I know in the areas where I have some expertise, I easily overthink things and appreciate it when 'lay' people point out an obvious answer to a problem. This usually comes in the form of a question: "Why can't you just do 'X'?" (usually thinking there must be a good reason why I hadn't already suggested 'X').

Hence me trying to illustrate that the underlying structure implied by a Nullable(Of Object) is simple, and by implication, wonder out loud about why Classes and Structures should be treated so differently.

e.g. if we have:

Dim nullableInt As Integer?
Console.WriteLine(nullableInt.HasValue)

Why can't we have

Dim someObject as Object?
Console.WriteLine(someObject.HasValue)

?

As a minor bonus: if that were allowed, it would allow for a structure to be converted to class without breaking code.

Similarly, I can see that something like this would be handy:

Dim someObject as Object!
someObject = Nothing   ' throws NullReferenceException
tverweij commented 5 years ago

I understand the hesitation to add nonnullable references to VB. Because what happens when I do the following: NonNullableObject = ThirdPartyDll.SomeMethod

If the method returns a null value - what happens then? You can not check this with the compiler and a runtime error wouldn't be nice. So, after this call, the NonNullableObject could contain a null value. That is why I don't like the C# implementation; it does not garantee anything as soon as you call third party code.

I'd like to see another approach; Non nullable Objects instead of Non nullable References. Each non nullable object must have a static method that constructs a default value for that object; for nullable objects the default value is Null, for non nullable objects, you have to define the default value. For strings this would be the VB6 string: an uninitialized non nullable string contains an empty string (""). For all other objects it should be defined in the class definition.

Implementation proposal: Public Shared Function DefaultValue As MyClassName

For existing objects in the runtime and in third party dll's, this method can be added using an extension method. As soon as the (extension) method exists, the class can be defined as non nullable.

The compiler must add a check to every assignment outside your own code to see if the result is a null value, and if it is, replace it by the default value.

pricerc commented 5 years ago

I understand the hesitation to add nonnullable references to VB. Because what happens when I do the following: NonNullableObject = ThirdPartyDll.SomeMethod

If the method returns a null value - what happens then? You can not check this with the compiler and a runtime error wouldn't be nice.

But a runtime error would be correct. One should always 'not trust' a third party DLL anyway, so you'd wrap that in a try/catch NullReferenceException.

That is why I don't like the C# implementation; it does not garantee anything as soon as you call third party code.

This was covered by Mads in his comments about it.

I'd like to see another approach; Non nullable Objects instead of Non nullable References.

As I read it, that was also discussed. I think that was the 'ideal', and what they've implemented is the closest they can get to realising that ideal, while still remaining 'backwards compatibility'.

Implementation proposal: Public Shared Function DefaultValue As MyClassName

For existing objects in the runtime and in third party dll's, this method can be added using an extension method. As soon as the (extension) method exists, the class can be defined as non nullable.

The compiler must add a check to every assignment outside your own code to see if the result is a null value, and if it is, replace it by the default value.

Generally, I like how you're thinking, but (playing devil's advocate here) what's to stop DefaultValue returning null ?

tverweij commented 5 years ago

Generally, I like how you're thinking, but (playing devil's advocate here) what's to stop DefaultValue returning null ?

Nothing (lol). But because you define it yourself, it is your own problem. That is the only way to get a null value into a non nullable object, but doing that would be silly don't you think?

pricerc commented 5 years ago

Generally, I like how you're thinking, but (playing devil's advocate here) what's to stop DefaultValue returning null ?

Nothing (lol). But because you define it yourself, it is your own problem. That is the only way to get a null value into a non nullable object, but doing that would be silly don't you think?

I was thinking more of a third-party dll; what's to stop someone else from returning null in their implementation of DefaultValue ?

That's the main problem highlighted previously: somewhere along the line, there's going to be a null that you're going to have to deal with; we can't just pretend that they don't exist.

I think 'Not Nullable' is mostly interesting in the context of method parameters: As you said: "it would be silly" to write your own breakable code.

That's why I like the idea of the ? (Nullable(Of Object)) and ! (NotNullable(Of Object)) decorators someone came up with, which become compiler 'hints' for "Null values for this parameter are permitted, and I will handle nulls manually" and "Null values for this parameter are not permitted, please add null argument guard clauses to my method".

tverweij commented 5 years ago

was thinking more of a third-party dll; what's to stop someone else from returning null in their implementation of DefaultValue ?

In that case a runtime error will occur; NonNullableObjectException - the defaultvalue of the object returns a null value; this object can not be used as Non Nullable Object.

That's the main problem highlighted previously: somewhere along the line, there's going to be a null that you're going to have to deal with; we can't just pretend that they don't exist.

That is why there has to by a check on each assgnment to replace the null value with the default value - this way the Object will never be null.

I think 'Not Nullable' is mostly interesting in the context of method parameters

I think too; with a non nullable object, the null value will be replaced by the default value in such a function call. When a null value is send as parameter to a non nullable object paramter: If it is a ByVal argument, the function works with the non nullable object and the original is not changed, if it is a byRef parameter, the original object will be changed to the default value.

The point is that with Non Nullable Objects there is a garantee that the object is not null, ever.

Bill-McC commented 5 years ago

Please, no. There is not a default person that has a default address. Sure a customer should not be null, but there is not a default one. And if I have a MustInherit (abstract) Vehicle class, I don't want a default, I want car, truck, bicycle etc. Again it is not the object type that is not nullable, it always is. It is the reference assignments that are agressively checked by the compiler to limit runtime null exceptions. The very idea that there would be a runtime NonNullableObjectException highlights this is not the correct view on minimising runtime exceptions and simplifying writing code that includes null checks.

tverweij commented 5 years ago

@Bill-McC First: You don't have to use it. Second: The exception is only needed for 1 simple case - it should never occur Third: The most common error in software (not only VB) is a null reference. This will eliminate this error.

And if I have a MustInherit (abstract) Vehicle class, I don't want a default, I want car, truck, bicycle etc.

Each of these classes can have there own default.

tverweij commented 5 years ago

I moved this to #386.

Bill-McC commented 5 years ago

So, I gave the example where I have customer that should not be null but there is no default customer with a default address. Checking against a default is more computationally expensive and no better than checking for null. In fact it would leave huge holes that would be harder to detect. So yes, 1, I don't have to use it. Problem solved ?

tverweij commented 5 years ago

Only use it where it makes sense (as for all language features).

tverweij commented 5 years ago

Just for the record: I am not advocating for the replacement of nullable references. They have there use. I am advocating for the possibility to define another default than null for a reference object.

Bill-McC commented 5 years ago

For defaults I would suggest using factory method approach. It will be limited to the types you define, but that would be the case anyway. For nulls, I still think being able to declare compiler checking for possible nulls, such as on parameters, would be helpful.

jdmichel commented 5 years ago

I still think something like my original proposal is simplest.

Sub Pick(x As Animal)
    If TypeOf x Is Monkey Then
        Dim a = DirectCast(x, Monkey) ' This cast is still needed 
    End If
End Sub

Seems analogous to:

Sub Pick(aref As Apple?) 
    If aref IsNot Nothing Then
        Dim n = aref!  ' We're essentially casting away the null
        Dim n As Apple = If(aref) ' Natural extension of If used for null coalesce?
    End If
End Sub

There is no problem with accidentally getting null from any existing code or library, because prior to this feature all code written used nullable references. With this approach we just assume every reference object may be null, so it's a compile error to work with it until you cast away the nullness.

Dim px As SomeObj = SomeLib.SomeFunc() ' compile error
Dim px As SomeObj? = SomeLib.SomeFunc() ' This is fine
If px IsNot Nothing Then
     Dim x = If(px)
     Sub1(x)
     Sub2(x)
     SubN(x)
End If

What we gain is that all of the code in our library can be written to use non-nullable reference parameters, which lets our maintainers know that the compiler guarantees that someone has already cast away the nullness (presumably after verifying the reference isn't null). This makes all our code simpler, and is something that C++ developers have been doing for decades.

Furthermore, if someone takes the time to use this feature in the library, then they may add an annotation to SomeFunc indicating it can't be null, and we can then use it. But we won't be forced to, because at worst we're then doing a redundant null check.

The only revision I'd make to my original proposal is that I would explicitly state the assumption that non-nullable references should be immutable. (Mostly because that's what I'm used to, but I think it prevents several corner case problems) That is:

    Dim a As Apple = CreateApple(1)
    Dim b As Apple = CreateApple(2)
    a = b ' Illegal because a can't be reassigned
    Dim c As Apple ' Illegal because c wasn't assigned a reference

The only thing this feature would do is cause you to get lots of errors (or warnings) for code you write that fails to explicitly handle null. However, once libraries begin to have meta-data added to indicate things that have already been checked, then the there shouldn't be many errors. So you could turn this feature on safely for your own code in new projects, and then turn it on in a few years once the majority of nuget packages are updated with non-nullable attribute meta-data.

Maybe this would be too difficult to implement now, but it seems like a subset of what C# did, and therefore must be less work. If you did decide to someday duplicate exactly what C# did, then it would be possible to do this part first, and then try it out before deciding to do all the extra flow control work.

pricerc commented 5 years ago

It's late where I am, but a couple of thoughts (I might come back for more tomorrow):

Sub Pick(x As Animal)
    If TypeOf x Is Monkey Then
        Dim a = DirectCast(x, Monkey) ' This cast is still needed 
    End If
End Sub

That's completely valid VB today - if you pass Null to Pick, TypeOf(x) will not be a Monkey.

Sub Pick(aref As Apple?) 
    If aref IsNot Nothing Then
        Dim n = aref!  ' We're essentially casting away the null
        Dim n As Apple = If(aref) ' Natural extension of If used for null coalesce?
    End If
End Sub

You have "Dim n" in there twice , is that two variations on the same theme?.

Personally I'd rather see

Sub Pick(aref As Apple?) 
    If aref.HasValue Then
        Dim n = aref.Value 
    End If
End Sub

Since VB has the concept of ! as a 'dereference operator', I don't see why aref! can't be a shortcut for aref.Value (and the same for nullable(Of Structure), for that matter). Although the parser is probablye currently expecting something after the !