وصف المدون

إعلان الرئيسية

أخبار ساخنة

Home How the Newest Version of C# Handles Null References

How the Newest Version of C# Handles Null References

إعلان أول الموضوع

a history of advancements in the software design and implementation of the language's safety.

Manipulating null references, a programming concept that many developers find tedious, is one of these improvements.

Null references can lead to a variety of problems in your code, such as information gaps and exceptions.

You will learn how to deal with null references in the most recent C# and.NET programming language version from this article. Let no null pass go unattended is the name of the game.

There will be multiple stages to this demonstration, each with a brief demo. Please use the table of contents below to navigate around.

Prerequisites

You must first fulfill a few requirements in order to continue. You've probably written enough C# code to be able to recognize null references in their native environment. And I anticipate that you will recognize that they may pose a risk to the stability and design of the code.

This article will use C# syntax and libraries to explain these ideas, identify problems, and provide solutions.

We can begin working with nullable reference types if you're ready. This will enable us to prepare for the upcoming, more complicated demos by setting up the workspace and getting everyone up to speed.

How to Use Nullable Reference Types

Introduced in C# 8, nullable reference types quickly gained popularity.

In summary, a reference can be declared as either nullable (string s, for instance) or non-nullable (string s).

Take note of the plot twist: prior to C# 8, string s was just a reference to a nullable string; however, it has now evolved into something more: a reference that is never meant to be set to null.

That was the revolutionary shift—possibly the first in ten years of C# syntax development!

The compiler will attempt to determine whether each assignment to a non-nullable reference—that is, the reference without a question mark—sets it to the correct object. The compiler will produce a compile-time warning if it discovers an execution path that could set it to null. The compiler attempts to demonstrate that each non-nullable reference is unquestionably assigned to an object in what is known as "definite assignment analysis."

I would like to know if you would consider not using nullable reference types today if you have already become accustomed to them. Most likely not.

Let's write a little code first. Two records are shown below, one of which is derived from the other. C# 9 came with record types. I have only used them here out of necessity. Think of these two types as the base and the derived class, respectively.

record Person(string FirstName, string LastName);

record Celebrity(string FirstName, string LastName, string KnownFor)
    : Person(FirstName, LastName);

We have two options: we can assign a reference to null or instantiate a record and assign the instance to a reference.

The definite assignment analysis enters the picture at this point. We must use the question mark to indicate that the reference may be null if there is a set of instructions that leads to that outcome.

Person? left = null;

Person? bob = new Person("Bob", "Coder");

Person fowler = new Celebrity("Martin", "Fowler", "famous books");

Person martin = new Celebrity("Bob", "Martin", "SOLID principles");

As you can see, bob, the second reference, is declared nullable even though he is assigned to a proper object. In situations where an object is approaching from the outside and you are unsure if it will be present or not, that is acceptable.

It is important to ensure that a non-nullable reference is not assigned a nullable one. That will result in a compile-time warning, which you can choose to escalate to a compile-time error.

It is crucial to realize that nullability is an indication provided to the compiler rather than a feature of the type. Nullable reference types are never stored in the compiled type itself; they are only used in the compile-time analysis.

The twist comes in that, as shown in the code below, we can freely mark any reference to the generic parameter type as nullable. Just like any other reference, this one is open to final assignment analysis.

void Showcase<T>(string caption, Action<T?> action, params T?[] objects)

{

    Console.WriteLine($"Showcasing {caption}:".ToUpper());

    foreach (T obj in objects) action(obj);

    Console.WriteLine();

}

For the remainder of this piece, we have defined the utility function to illustrate every scenario that includes null references. Declaring this generic function as Showcase<T?> would be a compile-time error, as I have already mentioned, even though it would be perfectly acceptable to accept a nullable T? in the argument list. causes your head to spin!

What comes next is even more puzzling: why not take nullable out of the argument list? What would that entail?

void Showcase<T>(string caption, Action<T> action, params T[] objects)
{
    Console.WriteLine($"Showcasing {caption}:".ToUpper());
    foreach (T obj in objects) action(obj);
    Console.WriteLine();
}

That would leave the decision of whether or not to be nullable to the caller because, listen carefully now, a concrete generic parameter type can be nullable. During compilation, it ascertains the nullability of references, which is a real thing.

I hope that by now you are beginning to understand these ideas more. I would definitely encourage you to study more about nullability of types, but it would take up a lot of space to go into detail on this topic. It is a permanent feature of C# now.

Let me give you a quick demo showcasing the two possible choices:

Showcase<Person?>("Nullable reference types", Console.WriteLine,
                  left, bob, fowler, martin);
Showcase<Person>("Non-nullable reference types", Console.WriteLine,
                  fowler, martin);

While the second call above requires non-nullable references, the first call above permits null references in the arguments. In that scenario, the compiler would examine the references supplied as arguments and issue a warning if any of them were or might be null.

Our quick tutorial on C# nullable reference types is now complete. We are prepared to move forward with more complex issues.

Before that, here is the output produced by the code as we have it so far:

SHOWCASING NULLABLE REFERENCE TYPES:
                                                    <-- null is here!
Person { FirstName = Bob, LastName = Coder }
Celebrity { FirstName = Martin, LastName = Fowler, KnownFor = famous books }
Celebrity { FirstName = Bob, LastName = Martin, KnownFor = SOLID principles }

SHOWCASING NON-NULLABLE REFERENCE TYPES:
Celebrity { FirstName = Martin, LastName = Fowler, KnownFor = famous books }
Celebrity { FirstName = Bob, LastName = Martin, KnownFor = SOLID principles }

Pay attention to the empty line in the output. That is where we have passed null to the Console.WriteLine. The WriteLine method accepts null and treats it the same way it treats an empty string.

How to Use the is null and is not null Patterns

As soon as nullability is understood, we can begin to construct logic around it. To find out if a reference is equal to null is the most basic operation of all.

void IsNull(Person? person)
{
    if (person is null)
        Console.WriteLine("Sad to see you leaving.");

    if (person is not null)
        Console.WriteLine($"Everybody say hello to {person}"!);
}

Showcase("is null and is not null patterns", IsNull,
         left, bob, fowler, martin);

An object is being tested by the is operator against a pattern. This operator will come up again and again in the next sections.

Its most basic application—testing against the null pattern—is demonstrated in this demo. There are two possibilities with meanings that seem to be self-explanatory: is null and is not null. Yes, but that would be a grave error!

Is null and is not null patterns cover corner cases, which may have been the primary motivation behind their initial introduction. Calling any overload of the == and!= operators will be avoided by both patterns.

Theoretically, a class could declare that a specific object should be treated as equal to a null reference by overloading the == and!= operators. However, since the is null pattern won't invoke the operator overload, it will categorically reject comparing the identical non-null object to null.

Although it is a small corner case, it demonstrates how C# functions internally. In testing for null/non-null, the general rule is to favor is null over == and is not null over!=.

This printout results from using the aforementioned function on a few references, one of which is null.

SHOWCASING IS NULL AND IS NOT NULL PATTERNS:
Sad to see you leaving.
Everybody say hello to Person { FirstName = Bob, LastName = Coder }
Everybody say hello to Celebrity { FirstName = Martin, LastName = Fowler, KnownFor = famous books }
Everybody say hello to Celebrity { FirstName = Bob, LastName = Martin, KnownFor = SOLID principles }

How to Use Type-Test-and-Set Patterns

It's time to step up and employ some of the more sophisticated techniques for handling nullable references. We'll stick with the is operator, but we'll use testing type patterns, which is a more powerful version of it.

In C#, every reference resolves to an object—or null, if it doesn't—and every object we reference has the type descriptor. It is the fundamental idea behind all object-oriented languages.

The.NET runtime can thus easily determine whether a reference points to an object and, if so, whether the runtime type of that object either directly or indirectly derives from a particular type.

That was a mouthful, wasn't it? Let's split that up into bits:

  • To test whether a reference references an actual object, that is the person is not null pattern.
  • To add the test whether that object is assignable to a particular type, we use the type pattern instead: person is Celebrity.
  • Finally, to capture the reference to the desired type and use it in subsequent statements and expressions, we use the full-blown type-test-and-set expression: person is Celebrity celeb.

These are the three steps—each more effective than the last—of information extraction from a reference.

Here is the method that exercises the most detailed form, all contained in one condensed expression: testing against null and downcasting.

void TypeTestAndSet(Person? person)
{
    string report = person switch
    {
        Celebrity celebrity =>
            $"{celebrity.FirstName} {celebrity.LastName} known for {celebrity.KnownFor}",
        Person commonPerson =>
            $"{commonPerson.FirstName} {commonPerson.LastName}",
        _ => string.Empty,
    };
    if (!string.IsNullOrEmpty(report)) Console.WriteLine(report);

    if (person is Celebrity celeb) Console.WriteLine("*** Did you see a celebrity?");
}

Showcase("Type test and set patterns", TypeTestAndSet,
         left, bob, fowler, martin);

It's possible that you've noticed how well these expressions use safe downcasting. For many years, downcasting was discouraged because it was believed—mostly correctly—to be the source of design and code flaws.

However, things are changing! Functional programming is where type tests and set expressions originate in software development.

The differences between type testing and downcasting as they were implemented in older object-oriented languages are not appropriate topics for this article to cover. Before passing judgment, I implore you to educate yourself further on this fascinating subject.

SHOWCASING TYPE TEST AND SET PATTERNS:
Bob Coder
Martin Fowler known for famous books
*** Did you see a celebrity?
Bob Martin known for SOLID principles
*** Did you see a celebrity?

The output generated by the aforementioned function is shown here. As you can see, every actual type is accurately captured and produces a unique output. The dreaded null was also omitted: although I did pass a null reference to the function at one point, it was ignored because it didn't match any of the patterns.

person switch
{
    Person commonPerson =>
        $"{commonPerson.FirstName} {commonPerson.LastName}",
    Celebrity celebrity =>              // <-- error
        $"{celebrity.FirstName} {celebrity.LastName} known for {celebrity.KnownFor}",
    _ => string.Empty,
};

How to Use Property Patterns

Extending pattern matching yields an exciting new discovery. A particular variation is the properties pattern, which tries to match the characteristics and values of an object's properties (assuming the object even exists!).

void PropertyPatterns(Person? person)
{
    if (person is { FirstName: "Bob"})
        Console.WriteLine($"Greet Bob, the one and only {person.FirstName} {person.LastName}!");
    else
        Console.WriteLine("Not a Bob");
}

Showcase("Property patterns", PropertyPatterns,
         left, bob, fowler, martin);

If you are not interested in downcasting, you do not need to specify the type. It will be the kind of reference that is located to the operator's left.

However, employing the is operator suggests a null test. On the right side of the expression, any reference that passes the is test will be non-null and safe to check its property values.

As a result, we interpret the condition of this instruction as follows: In the event that person is not null and Bob is the value of its FirstName property, then...

Here is the output produced when we call the function above:

SHOWCASING PROPERTY PATTERNS:
Not a Bob
Greet Bob, the one and only Bob Coder!
Not a Bob
Greet Bob, the one and only Bob Martin!

How to Use the Null Propagation and Null Coalescing Operators

Thus far, our approach has involved manipulating objects, which is unnatural in an object-oriented design. Recall that in object-oriented programming, we, as the object's users, only call its methods; the object itself exposes behavior.

When the reference we anticipate to point to an object is nullable, the issues still arise. A careless call on the null reference was the main cause of errors. But we should be safe from the dreaded NullReferenceExceptions now that we have nullable references and definite assignment checks taken care of for us.

Consider having a method exposed by the class. We can use ToString as a simple example.

record Person(string FirstName, string LastName)
{
    public override string ToString() =>
        $"{FirstName} {LastName}";
}

record Celebrity(string FirstName, string LastName, string KnownFor)
    : Person(FirstName, LastName)
{
    public override string ToString() =>
        $"{base.ToString()} known for {KnownFor}";
}

The difference between calling ToString on Person and on Person? types is significant. Since the latter is nullable, a careless call could result in the dereferencing of a null reference and, as you can imagine, a NullReferenceException.

Person a = ...;

Person? b = ...;

string x = a.ToString();      // safe

string y = b.ToString();      // unsafe

Enter the null-propagation operator (?.)! We can safely make an optional call to a method, provided the reference is non-null.

Person a = ...;

Person? b = ...;

string x = a.ToString();      // safe

string? y = b?.ToString();    // safe

However, note the results. The call on a null reference will be ignored if the method returns void. The nullable version of the type will be the outcome if the method returns a type. You understand that you cannot expect a string from ToString on a nullable reference? Instead, all the compiler can guarantee is a nullable string.

What if we truly desired a string, an authentic one? The null-coalescing operator (??) is entered! By giving a default value to use in the event that the actual value is null at run time, we can quickly transform a nullable reference into a non-nullable one.

void NullPropagationAndCoalescing(Person? person)
{
    string report = person?.ToString() ?? string.Empty;
    if (!string.IsNullOrEmpty(report)) Console.WriteLine(report);
}

Showcase("Null propagation and null coalescing operators",
         NullPropagationAndCoalescing,
         left, bob, fowler, martin);

In this example, we first call the optional ToString method, but if the reference is null, we short-circuit the result to an empty string. Any null reference would therefore result in an empty string being printed out.

SHOWCASING NULL PROPAGATION AND NULL COALESCING OPERATORS:
                            <-- An empty string printed here
Bob Coder
Martin Fowler known for famous books
Bob Martin known for SOLID principles

How to Work With Optional Objects

In actuality, nulls won't be used in the final strategy for handling nulls in this article. One more puzzle! By representing the objects as potentially missing, the goal is to completely avoid nulls. Keep in mind that the word "possibly" will be included in the type declaration, just like nullability was.

This brief explanation won't even come close to teaching you everything there is to know about optional objects if you're new to them. Optional objects are not natively supported in C#. One of the many implementations that NuGet offers is the LanguageExt library, which is the most widely used.

dotnet add package LanguageExt.Core

An object of any kind that is either present or absent is called an optional object. The optional object itself will always exist, regardless of the circumstances. You have another puzzle to solve!

Here is how we would declare a few optional objects:

using LanguageExt;

Option<Person>[] maybePeople = 

{

    Option<Person>.None,

    Option<Person>.Some(bob),

    Option<Person>.Some(fowler),

    Option<Person>.Some(martin),

};

Typically, the two forms of an optional object are called None and Some. An actual object must be present in the Some variant. The code that will never have a null reference and optional object creation are now complete.

However, how does this differ from nullable references? Why even should we utilize optional objects?

The short story is that, if the optional object has content, we can apply functions to it. If there is no content, the optional object will either call the function without passing any data to it or invoke it and pass the content to it.

Consequently, an optional type is a single instance of that calling protocol, which is analogous to safely dereferencing nullable references in many respects.

void Optional(Option<Person> maybePerson)
{
    string report = maybePerson.Match(
        person => person.ToString(),
        string.Empty);
    maybePerson.Do(Console.WriteLine);
}

Showcase("Optional objects", Optional, maybePeople);

The Match method handles both scenarios: in the event that the person is absent, it either replaces the missing person with an empty string or maps the Person object to a string. If content is present, the Do method will only pass it to the console.

Here is the printout produced by the Do method:

SHOWCASING OPTIONAL OBJECTS:

Bob Coder

Martin Fowler known for famous books

Bob Martin known for SOLID principles

Printing only the Some variants is what you will see. Because the optional instance has disregarded the action passed to its Do method, the lone missing object in the input array has not generated any output.

The ability of optional objects to apply other functions makes them a superior choice over nullable references. It's possible that our codebase already contains a wide variety of classes and methods, all of which rely on non-nullable references. Using optional objects can help close the gap between common methods that only work when nothing is missing and possibly missing objects.

Final Notes

In this tutorial, we started by declaring nullable objects and testing their existence using the is operator.

Then, we extended the example by displaying the richness of pattern-matching expressions: type test and set and property pattern expressions.

We then moved the focus from consuming objects to calling their behavior from the null-propagation operator over the null-coalescing operator, landing in the vast field of functional programming and optional objects.

I hope you enjoyed the ride.

إعلان أخر الموضوع

التصنيفات:
تعديل المشاركة
إقرأ أيضاً × +
No comments
Post a Comment

إعلان وسط الموضوع

Back to top button

مرر لأسفل