Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
The C# Workshop

You're reading from   The C# Workshop Kickstart your career as a software developer with C#

Arrow left icon
Product type Paperback
Published in Sep 2022
Publisher Packt
ISBN-13 9781800566491
Length 780 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Almantas Karpavicius Almantas Karpavicius
Author Profile Icon Almantas Karpavicius
Almantas Karpavicius
Jason Hales Jason Hales
Author Profile Icon Jason Hales
Jason Hales
Mateus Viegas Mateus Viegas
Author Profile Icon Mateus Viegas
Mateus Viegas
Arrow right icon
View More author details
Toc

Conditional Operators

A null reference exception is probably the most common error in programming. For example, refer to the following code:

int[] numbers = null;
numbers.length;

This code will throw NullReferenceException because you are interacting with a variable that has a null value. What is the length of a null array? There is no proper answer to this question, so an exception will be thrown here.

The best way to protect against such an error is to avoid working with null values altogether. However, sometimes it is unavoidable. In those cases, there is another technique called defensive programming. Before using a value that might be null, make sure it is not null.

Now recall the example of the Dog class. If you create a new object, the value of Owner could be null. If you were to determine whether the owner's name starts with the letter A, you would need to check first whether the value of Owner is null, as follows:

if (dog.Owner != null)
{
    bool ownerNameStartsWithA = dog.Owner.StartsWith('A');
}

However, in C#, using null-conditional, this code becomes as simple as the following:

dog.Owner?.StartsWith('A');

Null-conditional (?) is an example of conditional operators in C#. It is an operator that implicitly runs an if statement (a specific if statement is based on the operator) and either returns something or continues work. The Owner?.StartsWith('A') part returns true if the condition is satisfied and false if it is either not satisfied or the object is null.

There are more conditional operators in C# that you will learn about.

Ternary Operators

There is hardly any language that does not have if statements. One of the most common kinds of if statement is if-else. For example, if the value of Owner is null for an instance of the Dog class, you can describe the instance simply as {Name}. Otherwise, you can better describe it as {Name}, dog of {Owner}, as shown in the following snippet:

if (dog1.Owner == null)
{
    description = dog1.Name;
}
else
{
    description = $"{dog1.Name}, dog of {dog1.Owner}";
}

C#, like many other languages, simplifies this by using a ternary operator:

description = dog1.Owner == null
    ? dog1.Name
    : $"{dog1.Name}, dog of {dog1.Owner}";

On the left side, you have a condition (true or false), followed by a question mark (?), which returns the value on the right if the condition is true, followed by a colon (:), which returns the value to the left if the condition is false. $ is a string interpolation literal, which allows you to write $"{dog1.Name}, dog of {dog1.Owner}" over dog1.Name + "dog of" + dog1.Owner. You should use it when concatenating text.

Imagine there are two dogs now. You want the first dog to join the second one (that is, be owned by the owner of the second dog), but this can only happen if the second one has an owner to begin with. Normally, you would use the following code:

if (dog1.Owner != null)
{
    dog2.Owner = dog1.Owner;
}

But in C#, you can use the following code:

dog1.Owner = dog1.Owner ?? dog2.Owner;

Here, you have applied the null-coalescing operator (??), which returns the value to the right if it is null and the value on the left if it is not null. However, you can simplify this further:

dog1.Owner ??= dog2.Owner;

This means that if the value that you are trying to assign (on the left) is null, then the output will be the value on the right.

The last use case for the null-coalescing operator is input validation. Suppose there are two classes, ComponentA and ComponentB, and ComponentB must contain an initialized instance of ComponentA. You could write the following code:

public ComponentB(ComponentA componentA)
{
    if (componentA == null)
    {
        throw new ArgumentException(nameof(componentA));
    }
    else
    {
        _componentA = componentA;
    }
}

However, instead of the preceding code, you can simply write the following:

_componentA = componentA ?? throw new ArgumentNullException(nameof(componentA));

This can be read as If there is no componentA, then an exception must be thrown.

Note

You can find the code used for this example at https://packt.link/yHYbh.

In most cases, null operators should replace the standard if null-else statements. However, be careful with the way you use the ternary operator and limit it to simple if-else statements because the code can become unreadable very quickly.

Overloading Operators

It is fascinating how much can be abstracted away in C#. Comparing primitive numbers, multiplying, or dividing them is easy, but when it comes to objects, it is not that simple. What is one person plus another person? What is a bag of apples multiplied by another bag of apples? It is hard to say, but it can make total sense in the context of some domains.

Consider a slightly better example. Suppose you are comparing bank accounts. Finding out who has more money in a bank account is a common use case. Normally, to compare two accounts, you would have to access their members, but C# allows you to overload comparison operators so that you can compare objects. For example, imagine you had a BankAccount class like so:

public class BankAccount
{
    private decimal _balance;
 
    public BankAccount(decimal balance)
    {
        _balance = balance;
    }
}

Here, the balance amount is private. You do not care about the exact value of balance; all you want is to compare one with another. You could implement a CompareTo method, but instead, you will implement a comparison operator. In the BankAccount class, you will add the following code:

public static bool operator >(BankAccount account1, BankAccount account2)
    => account1?._balance > account2?._balance;

The preceding code is called an operator overload. With a custom operator overload like this, you can return true when a balance is bigger and false otherwise. In C#, operators are public static, followed by a return type. After that, you have the operator keyword followed by the actual operator that is being overloaded. The input depends on the operator being overloaded. In this case, you passed two bank accounts.

If you tried to compile the code as it is, you would get an error that something is missing. It makes sense that the comparison operators have a twin method that does the opposite. Now, add the less operator overload as follows:

public static bool operator <(BankAccount account1, BankAccount account2)
    => account1?._balance < account2?._balance;

The code compiles now. Finally, it would make sense to have an equality comparison. Remember, you will need to add a pair, equal and not equal:

public static bool operator ==(BankAccount account1, BankAccount account2)
    => account1?._balance == account2?._balance; 
public static bool operator !=(BankAccount account1, BankAccount account2)
    => !(account1 == account2);

Next, you will create bank accounts to compare. Note that all numbers have an m appended, as this suffix makes those numbers decimal. By default, numbers with a fraction are double, so you need to add m at the end to make them decimal:

var account1 = new BankAccount(-1.01m);
var account2 = new BankAccount(1.01m);
var account3 = new BankAccount(1001.99m);
var account4 = new BankAccount(1001.99m);

Comparing two bank accounts becomes as simple as this now:

Console.WriteLine(account1 == account2);
Console.WriteLine(account1 != account2);
Console.WriteLine(account2 > account1);
Console.WriteLine(account1 < account2);
Console.WriteLine(account3 == account4);
Console.WriteLine(account3 != account4);

Running the code results in the following being printed to the console:

False
True
True
True
True
False

Note

You can find the code used for this example at https://packt.link/5DioJ.

Many (but not all) operators can be overloaded, but just because you can do so does not mean you should. Overloading operators can make sense in some cases, but in other cases, it might be counterintuitive. Again, remember to not abuse C# features and use them when it makes logical sense, and when it makes code easier to read, learn, and maintain.

Nullable Primitive Types

Have you ever wondered what to do when a primitive value is unknown? For example, say a collection of products are announced. Their names, descriptions, and some other parameters are known, but the price is revealed only before the launch. What type should you use for storing the price values?

Nullable primitive types are primitive types that might have some value or no value. In C#, to declare such a type, you have to add ? after a primitive, as shown in the following code:

int? a = null;

Here, you declared a field that may or may not have a value. Specifically, this means that a can be unknown. Do not confuse this with a default value because, by default, the value of int types is 0.

You can assign a value to a nullable field quite simply, as follows:

a = 1;

And to retrieve its value afterward, you can write the code as follows:

int b = a.Value;

Generics

Sometimes, you will come across situations where you do the exact same thing with different types, where the only difference is because of the type. For example, if you had to create a method that prints an int value, you could write the following code:

public static void Print(int element)
{
    Console.WriteLine(element);
}
If you need to print a float, you could add another overload:
public static void Print(float element)
{
    Console.WriteLine(element);
}

Similarly, if you need to print a string, you could add yet another overload:

public static void Print(string element)
{
    Console.WriteLine(element);
}

You did the same thing three times. Surely, there must be a way to reduce code duplication. Remember, in C#, all types derive from an object type, which has the ToString() method, so you can execute the following command:

public static void Print(object element)
{
    Console.WriteLine(element);
}

Even though the last implementation contains the least code, it is actually the least efficient. An object is a reference type, whereas a primitive is a value type. When you take a primitive and assign it to an object, you also create a new reference to it. This is called boxing. It does not come for free, because you move objects from stack to heap. Programmers should be conscious of this fact and avoid it wherever possible.

Earlier in the chapter, you encountered polymorphism—a way of doing different things using the same type. You can do the same things with different types as well and generics are what enable you to do that. In the case of the Print example, a generic method is what you need:

public static void Print<T>(T element)
{
    Console.WriteLine(element);
}

Using diamond brackets (<>), you can specify a type, T, with which this function works. <T> means that it can work with any type.

Now, suppose you want to print all elements of an array. Simply passing a collection to a WriteLine statement would result in printing a reference, instead of all the elements. Normally, you would create a method that prints all the elements passed. With the power of generics, you can have one method that prints an array of any type:

public static void Print<T>(T[] elements)
{
    foreach (var element in elements)
    {
        Console.WriteLine(element);
    }
}

Please note that the generic version is not as efficient as taking an object type, simply because you would still be using a WriteLine overload that takes an object as a parameter. When passing a generic, you cannot tell whether it needs to call an overload with an int, float, or String, or whether there is an exact overload in the first place. If there was no overload that takes an object for WriteLine, you would not be able to call the Print method. For that reason, the most performant code is actually the one with three overloads. It is not terribly important though because that is just one, very specific scenario where boxing happens anyway. There are so many other cases, however, where you can make it not only concise but performant as well.

Sometimes, the answer to choosing a generic or polymorphic function hides in tiny details. If you had to implement a method for comparing two elements and return true if the first one is bigger, you could do that in C# using an IComparable interface:

public static bool IsFirstBigger1(IComparable first, IComparable second)
{
    return first.CompareTo(second) > 0;
}

A generic version of this would look like this:

public static bool IsFirstBigger2<T>(T first, T second)
    where T : IComparable
{
    return first.CompareTo(second) > 0;
}

The new bit here is where T : IComparable. It is a generic constraint. By default, you can pass any type to a generic class or method. Constraints still allow different types to be passed, but they significantly reduce the possible options. A generic constraint allows only the types that conform to the constraint to be passed as a generic type. In this case, you will allow only the types that implement the IComparable interface. Constraints might seem like a limitation on types; however, they expose the behavior of the constrained types that you can use inside a generic method. Having constraints enables you to use the features of those types, so it is very useful. In this case, you do limit yourself to what types can be used, but at the same time, whatever you pass to the generic method will be comparable.

What if instead of returning whether the first element is bigger, you needed to return the first element itself? You could write a non-generic method as follows:

public static IComparable Max1(IComparable first, IComparable second)
{
    return first.CompareTo(second) > 0
        ? first
        : second;
}

And the generic version would look as follows:

public static T Max2<T>(T first, T second)
    where T : IComparable
{
    return first.CompareTo(second) > 0
        ? first
        : second;
}

Also, it is worth comparing how you will get a meaningful output using each version. With a non-generic method, this is what the code would look like:

int max1 = (int)Comparator.Max1(3, -4);

With a generic version, the code would be like this:

int max2 = Comparator.Max2(3, -4);

Note

You can find the code used for this example at https://packt.link/sIdOp.

In this case, the winner is obvious. In the non-generic version, you have to do a cast. Casting in code is frowned upon because if you do get errors, you will get them during runtime and things might change and the cast will fail. Casting is also one extra action, whereas the generic version is far more fluent because it does not have a cast. Use generics when you want to work with types as-is and not through their abstractions. And returning an exact (non-polymorphic) type from a function is one of the best use cases for it.

C# generics will be covered in detail in Chapter 4, Data Structures and LINQ.

Enum

The enum type represents a set of known values. Since it is a type, you can pass it instead of passing a primitive value to methods. enum holds all the possible values, hence it isn't possible to have a value that it would not contain. The following snippet shows a simple example of this:

public enum Gender
{
    Male,
    Female,
    Other
}

Note

You can find the code used for this example at https://packt.link/gP9Li.

You can now get a possible gender value as if it were in a static class by writing Gender.Other. Enums can easily be converted to an integer using casting—(int)Gender.Male will return 0, (int)Gender.Female will return 1, and so on. This is because enum, by default, starts numbering at 0.

Enums do not have any behavior and they are known as constant containers. You should use them when you want to work with constants and prevent invalid values from being passed by design.

Extension Methods

Almost always, you will be working with a part of code that does not belong to you. Sometimes, this might cause inconvenience because you have no access to change it. Is it possible to somehow extend the existing types with the functionality you want? Is it possible to do so without inheriting or creating new component classes?

You can achieve this easily through extension methods. They allow you to add methods on complete types and call them as if those methods were natively there.

What if you wanted to print a string to a console using a Print method, but call it from a string itself? String has no such method, but you can add it using an extension method:

public static class StringExtensions
{
    public static void Print(this string text)
    {
        Console.WriteLine(text);
    }
}

And this allows you to write the following code:

"Hey".Print();

This will print Hey to the console as follows:

Hey

Note

You can find the code used for this example at https://packt.link/JC5cj.

Extension methods are static and must be placed within a static class. If you look at the semantics of the method, you will notice the use of the this keyword. The this keyword should be the first argument in an extension method. After that, the function continues as normal and you can use the argument with the this keyword as if it was just another argument.

Use extension methods to add (extend, but not the same extensions as what happens with inheritance) new behavior to existing types, even if the type would not support having methods otherwise. With extension methods, you can even add methods to enum types, which is not possible otherwise.

You have been reading a chapter from
The C# Workshop
Published in: Sep 2022
Publisher: Packt
ISBN-13: 9781800566491
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image