Mathias Graabeck
Mathias Graabeck

Reputation: 73

Nullable types and early stopping issue

In C# when having nullable objects such as Decimal?, Int?, etc. While doing null checks, but with early stopping.

The compiler does not track the nullability of such object (prevMaxSpeed in this case) beyond the scope of the if statement. Is there a better way of doing this?

This is a just a small simplified snippet of more complex code, I'm not willing to do nested if-statements to fix it, because of readability issues.

I know I can just cast the it, but I feel like this is redundant.

foreach (var something in somethings)
{
    var prevMaxSpeed = something.FirstOrDefault(x => x != null)?.speed;

    // early stopping
    if (!prevMaxSpeed.HasValue && !prevMaxSpeed.IsDefaultValue())
    {
        HandleMaxSpeedNoValue();
        continue;
    }

    sp.SP_Characteristics.StaticSpeedProfile.First().StaticSpeedProfileStart = new SSP_ComplexType()
    {
        SSP_Speed = prevMaxSpeed, // "Cannot implicitly convert type 'decimal?' to 'decimal'. An explicit conversion exists (are you missing a cast?)"
        SSP_SpeedSpecified = true,
    };
}

Upvotes: 1

Views: 188

Answers (3)

Jon Skeet
Jon Skeet

Reputation: 1503040

There's a difference here between nullable reference types and nullable value types. In your case, the compile-time type of the variable prevMaxSpeed is still decimal?, and you can't assign that to a decimal field or property. However, you can use pattern matching to obtain an appropriately typed variable variable:

// This introduces a variable prevMaxSpeedValue, of type decimal, which is
// definitely assigned after the if statement (because control within
// the if statement body never reaches the *end* of the body).
if (prevMaxSpeed is not decimal prevMaxSpeedValue)
{
    HandleMaxSpeedNoValue();
    continue;
}

sp.SP_Characteristics.StaticSpeedProfile.First().StaticSpeedProfileStart = new()
{
    SSP_Speed = prevMaxSpeedValue,
    SSP_SpeedSpecified = true,
};

Here's a short but complete program to demonstrate that:

List<decimal?> items = [1.5m, 1.3m, null, 2m];

foreach (decimal? item in items)
{
    if (item is not decimal value)
    {
        Console.WriteLine("Item was null");
        continue;
    }
    
    // Note: decimal.Round(item) won't compile
    decimal rounded = decimal.Round(value);
    Console.WriteLine($"{value} is rounded to {rounded}");
}

Note that if we were talking about reference types, then just if (prevMaxSpeed is not null) would be enough to convince the compiler that after that if statement, prevMaxSpeed would not be null, so could be assigned to a non-nullable variable without any warnings.

Upvotes: 6

IV.
IV.

Reputation: 9438

Reading carefully to understand your intent, what you seem to be getting at is to HandleMaxSpeedNoValue() if prevMaxSpeed is null or default. You asked:

Is there a better way of doing this?

In C# 9 you could eliminate redundancy:

MOCK

decimal SSP_Speed;
var somethings = new List<List<unk>>();
foreach (var something in somethings) // Your code verbatim
{
    // The `FirstOrDefault(...)` expression taken from your code 
    // tells us that `something` is a collection of some class 
    // having a member named `speed`, and that `something` can 
    // contain null instances of the class itself, and that early
    // stop should also occur if the class itself is null.
    if(something.FirstOrDefault(x => x != null)?.speed is { } prevMaxSpeed 
        &&
        prevMaxSpeed != default)  // OR: "!prevMaxSpeed.IsDefaultValue()"
    {
        SSP_Speed = prevMaxSpeed; // Assignment compiles now
    }
    else 
    {
        HandleMaxSpeedNoValue();
        continue;                // Early stop
    }
    // Subsequent loop processing
    // ...
}

void HandleMaxSpeedNoValue()
{
    /// ...
}
class unk
{
    public decimal? speed;
}

For < C# 9:

decimal SSP_Speed;
var somethings = new List<List<unk>>();
foreach (var something in somethings)
{
    if( something.FirstOrDefault(x => x != null)?.speed is decimal prevMaxSpeed 
        &&
        prevMaxSpeed != default)
    {
        SSP_Speed = prevMaxSpeed;
    }
    else 
    {
        HandleMaxSpeedNoValue();
        continue;                // Early stop
    }
    // Subsequent loop processing
    // ...
}
/// ...

Upvotes: 2

canton7
canton7

Reputation: 42330

Actually, the compiler does track nullability of nullable value types:

public void M(int? foo)
{
    // warning CS8629: Nullable value type may be null.
    Console.WriteLine(foo.Value);
}

public void N(int? foo)
{
    if (foo == null)
    {
        return;
    }
    // No warning
    Console.WriteLine(foo.Value);
}

See on SharpLab.

What doesn't change is the fact that you must access the Nullable<T>'s value using the .Value property. This is because Nullable<T> is a struct which has the properties .HasValue and .Value, and the compiler's view on whether the Nullable<T> could contain null at a given point in the code doesn't change this.

Upvotes: 5

Related Questions