89

While I was studying the delegate which is actually an abstract class in Delegate.cs, I saw the following method in which I don't understand

  • Why the return value uses ? though it's already a reference(class) type
  • ?[]? meaning on the parameter

Could you explain?

public static Delegate? Combine(params Delegate?[]? delegates)
{
    if (delegates == null || delegates.Length == 0)
        return null;

    Delegate? d = delegates[0];
    for (int i = 1; i < delegates.Length; i++)
        d = Combine(d, delegates[i]);

    return d;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 6
    https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references – GSerg Dec 29 '19 at 11:31
  • 23
    Isn't it a nullable array that can contain nullable values ? – Cid Dec 29 '19 at 11:35
  • 3
    c#8, you can now specify that an object variable is not allowed to be null. If you flip that compiler flag, you have to specify every variable that *is* allowed to be null. – Jeremy Lakeman Dec 29 '19 at 15:01

3 Answers3

68

Step by step explanation:

params Delegate?[] delegates - It is an array of nullable Delegate

params Delegate?[]? delegates - The entire array can be nullable

Since each parameter is of the type Delegate? and you return an index of the Delegate?[]? array, then it makes sense that the return type is Delegate? otherwise the compiler would return an error as if you were returing and int from a method that returns a string.

You could change for instance your code to return a Delegate type like this:

public static Delegate Combine(params Delegate?[]? delegates)
{
    Delegate defaulDelegate = // someDelegate here
    if (delegates == null || delegates.Length == 0)
        return defaulDelegate;

    Delegate d = delegates[0] ?? defaulDelegate;
    for (int i = 1; i < delegates.Length; i++)
        d = Combine(d, delegates[i]);

    return d;
}
Community
  • 1
  • 1
Athanasios Kataras
  • 25,191
  • 4
  • 32
  • 61
  • 4
    what about my first question? _Why the return value uses `?` though it's already reference(class) type_ – Soner from The Ottoman Empire Dec 29 '19 at 13:01
  • 2
    Because you are returning d which is Delegate? type. And also null or Delegate depending on the parameters – Athanasios Kataras Dec 29 '19 at 13:15
  • 2
    @snr The return value uses the `?` for exactly the same reason why the argument value uses the `?`. If you understand the latter, you automatically understand the former. Because the method *may return null*, like the parameter *may accept null*. That is, they both *may contain null*. – GSerg Dec 29 '19 at 13:18
  • @GSerg I've already grasped it by your comment. I mentioned in his answer cuz to know why he didn't touch the first point. – Soner from The Ottoman Empire Dec 29 '19 at 14:24
  • 2
    I touched it. I just answered it as the second part with the example. `Since each parameter is of the type Delegate? and you return an index of the Delegate?[]? array, then it makes sense that the return type is Delegate?` – Athanasios Kataras Dec 29 '19 at 14:37
  • 2
    @snr: _Why the return value uses ? though it's already reference(class) type_ (1) It may be a habit or a refactoring remnant if structs are also used in similar code (2) In C#8 reference types can be disallowed to be inherently nullable (thus requiring explicit nullability (3) `Foo?` has a neat `HasValue` property, whereas `Foo` need to be `== null` checked (4) It communicates to developers who read the method signature that nulls are a possible, to be expected, and correct outcome. (5) The code looks to be written by someone who is very fond of nullability and being explicit about it. – Flater Dec 29 '19 at 23:52
26

Nullable Reference Types are new in C# 8.0, they do not exist before.

It's a matter of documentation, and how warnings at compile-time are produced.

The exception "object not set to an instance of an object" exception is quiet common. But this is a runtime exception, it can partially discovered at compile time already.

For a regulate Delegate d you can always call

 d.Invoke();

meaning, you can code it, at compile time nothing will happen. It may raise exceptions at runtime.

While for a new Delegate? p this Code

 p.Invoke();

will produce a compiler warning. CS8602: Dereference of a possibly null reference unless you write:

 p?.Invoke();

what means, call only if not null.

So you document a variable may contain null or not. It raises warnings earlier and it can avoid multiple tests for null. The same what you have for int and int?. You know for sure, one is not null - and you know how to convert one to the other.

Holger
  • 2,446
  • 1
  • 14
  • 13
  • with the caveat that you don't know for sure that `Delegate` is not null. You just pretend that you know for sure (which is good enough in most cases). – Tim Pohlmann Dec 30 '19 at 09:45
  • @TimPohlmann As well as int i = null is impossible, also a string s = null is impossible (in C#8 with this breaking change). So it's little more than pretending something. For backward compatibility it's downgraded to a warning. If you upgrade the warning to an error, you know for sure it's not null. – Holger Dec 30 '19 at 09:52
  • 4
    Last time I checked NRTs are not 100% bullet proof. There are certain edge cases the compiler cannot detect. – Tim Pohlmann Dec 30 '19 at 09:53
  • Yes, but this is the same as for "variable not initialized" or "not all code paths returning a value" warnings. That are all 100% compile-time features, without detection at runtime. Unlike int?, I think they don't add extra memory to hold extra information. So let's say it's as reliable as intellisense. Pretty good. – Holger Dec 30 '19 at 10:00
  • 1
    If you have an `int` it can never be null. If you have a `Delegate` it can be null (for various reasons, e.g. reflection). It's usually safe to assume that `Delegate` (in C#8 with NRT enabled) is not null, but you never know for sure (where for `int` we know for sure). – Tim Pohlmann Dec 30 '19 at 10:08
  • The need for `Delegate?.Invoke()` stems from the unfortunate design of event handling in .NET; if there had been an `Event` value type which wrapped delegates, then `myEvent.Add(someMethod)` could have been thread-safe, and calling `myEvent.Invoke()` when `myEvent` has no events could have been a simple no-op. – supercat Dec 30 '19 at 17:57
5

In C# 8 one should explicitly mark reference types as nullable.

By default, those types are not able to contain null, kinda similar to value types. While this does not change how things work under the hood, the type checker will require you to do this manually.

Given code is refactored to work with C# 8, but it does not benefit from this new feature.

public static Delegate? Combine(params Delegate?[]? delegates)
{
    // ...[]? delegates - is not null-safe, so check for null and emptiness
    if (delegates == null || delegates.Length == 0)
        return null;

    // Delegate? d - is not null-safe too
    Delegate? d = delegates[0];
    for (int i = 1; i < delegates.Length; i++)
        d = Combine(d, delegates[i]);

    return d;
}

Here is an example of an updated code (not working, just an idea) leveraging this feature. It saved us from a null-check and simplified this method a bit.

public static Delegate? Combine(params Delegate[] delegates)
{
    // `...[] delegates` - is null-safe, so just check if array is empty
    if (delegates.Length == 0) return null;

    // `d` - is null-safe too, since we know for sure `delegates` is both not null and not empty
    Delegate d = delegates[0];

    for (int i = 1; i < delegates.Length; i++)
        // then here is a problem if `Combine` returns nullable
        // probably, we can add some null-checks here OR mark `d` as nullable
        d = Combine(d, delegates[i]);

    return d;
}
amankkg
  • 4,503
  • 1
  • 19
  • 30
  • 2
    `those types are not able to contain null`, note that it generates a compiler **warning**, not an error. The code will run fine (although it might generate NullReferenceExceptions). This is done with backwards compatibility in mind. – JAD Dec 31 '19 at 08:21