Sign In Start Free Trial
Account

Add to playlist

Create a Playlist

Modal Close icon
You need to login to use this feature.
  • Book Overview & Buying The C# Workshop
  • Table Of Contents Toc
The C# Workshop

The C# Workshop

By : Jason Hales, Almantas Karpavicius, Mateus Viegas
4.5 (14)
close
close
The C# Workshop

The C# Workshop

4.5 (14)
By: Jason Hales, Almantas Karpavicius, Mateus Viegas

Overview of this book

C# is a powerful, versatile language that can unlock a variety of career paths. But, as with any programming language, learning C# can be a challenging process. With a wide range of different resources available, it’s difficult to know where to start. That's where The C# Workshop comes in. Written and reviewed by industry experts, it provides a fast-paced, supportive learning experience that will quickly get you writing C# code and building applications. Unlike other software development books that focus on dry, technical explanations of the underlying theory, this Workshop cuts through the noise and uses engaging examples to help you understand how each concept is applied in the real world. As you work through the book, you'll tackle realistic exercises that simulate the type of problems that software developers work on every day. These mini-projects include building a random-number guessing game, using the publisher-subscriber model to design a web file downloader, creating a to-do list using Razor Pages, generating images from the Fibonacci sequence using async/await tasks, and developing a temperature unit conversion app which you will then deploy to a production server. By the end of this book, you'll have the knowledge, skills, and confidence to advance your career and tackle your own ambitious projects with C#.
Table of Contents (10 chapters)
close
close

Reference Types

Suppose you have an object and the object is not created, just declared, as follows:

Dog speedy;

What would happen if you tried accessing its Name value? Calling speedy.Name would throw a NullReferenceException exception because speedy is yet to be initialized. Objects are reference types, and their default value is null until initialized. You have already worked with value types, such as int, float, and decimal. Now you need to grasp that there are two major differences between value and reference types.

Firstly, value types allocate memory on the stack, whereas reference types allocate memory on the heap. The stack is a temporary place in memory. As the name implies, in a stack, blocks of memory are stacked on top of each other. When you call a function, all local function variables will end up on a single block of the stack. If you call a nested function, the local variables of that function will be allocated on another block of the stack.

In the following figure, you can see which parts of code will allocate memory in the stack during execution, and which will do so in the heap. Method calls (1, 8, 10) and local variables (2, 4) will be stored in the stack. Objects (3, 5) and their members (6) will be stored on the heap. Stacks use the Push method to allocate data, and Pop to deallocate it. When memory is allocated, it comes on top of the stack. When it is deallocated, it is removed from the top as well. You deallocate memory from the stack as soon as you leave the scope of a method (8, 10, 11). Heap is much more random, and Garbage Collector (GC) automatically (unlike some other languages, where you need to do it yourself), deallocates memory.

Note

GC is a massive topic in itself. If you want to find out more, please refer to the official Microsoft documentation at https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals.

Figure 2.1: Stack and heap comparison

Figure 2.1: Stack and heap comparison

Note

If you make too many nested calls, you will run into a StackoverflowException exception because the stack ran out of memory. Freeing up memory on the stack is just a matter of exiting from a function.

The second difference is that, when value types are passed to a method, their value is copied, while for reference types, only the reference is copied. This means that the reference type object's state is modifiable inside a method, unlike a value type, because a reference is simply the address of an object.

Consider the following snippet. Here, a function named SetTo5 sets the value of the number to 5:

private static void SetTo5(int number)
{
        number = 5;
}

Now, consider the following code:

int a = 2;
// a is 2
Console.WriteLine(a);
SetTo5(a);
// a is still 2
Console.WriteLine(a);

This should result in the following output:

2
2 

If you run this code, you find that the printed value of a is still 2 and not 5. This is because a is a value type that passed the value 2, and therefore its value is copied. Inside a function, you never work with the original; a copy is always made.

What about reference types? Suppose you add a field named Owner inside the Dog class:

public class Dog
{    public string Owner;
}

Create a function, ResetOwner, that sets the value of the Owner field for an object to None:

private static void ResetOwner(Dog dog)
{
    dog.Owner = "None";
}

Now, suppose the following code is executed:

Dog dog = new Dog("speedy");
Console.WriteLine(dog.Owner);
ResetOwner(dog);
// Owner is "None"- changes remain
Console.WriteLine(dog.Owner);

This should result in the following output:

speedy
None 

Note

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

If you try running this snippet of code yourself, you will first see the name speedy on one line and then None printed on another. This would change the dog's name, and the changes would remain outside the function. This is because Dog is a class, and a class is a reference type. When passed to a function, a copy of a reference is made. However, a copy of a reference points to the whole object, and therefore the changes that are made remain outside as well.

It might be confusing to hear that you pass a copy of a reference. How can you be sure you are working with a copy? To learn this, consider the following function:

private static void Recreate(Dog dog)
{
    dog = new Dog("Recreated");
}

Here, creating a new object creates a new reference. If you change the value of a reference type, you are working with a completely different object. It may be one that looks the same but is stored in a completely different place in memory. Creating an object for a passed parameter will not affect anything outside the object. Though this may sound potentially useful, you should generally avoid doing this as it can make code difficult to comprehend.

Properties

The Dog class has one flaw. Logically, you wouldn't want the name of a dog to be changed once it is assigned. However, as of now, there is nothing that prevents changing it. Think about the object from the perspective of what you can do with it. You can set the name of a dog (sparky.Name = "Sparky") or you can get it by calling sparky.Name. However, what you want is a read-only name that can be set just once.

Most languages take care of this through setter and getter methods. If you add the public modifier to a field, this means that it can be both retrieved (read) and modified (written). It isn't possible to allow just one of these actions. However, with setters and getters, you can restrict both read and write access. In OOP, restricting what can be done with an object is key to ensuring data integrity. In C#, instead of setter and getter methods, you can use properties.

In OOP languages (for example Java), to set or get the values of a name, you would write something like this:

public string GetName()
{
    return Name;
}
public string SetName (string name)
{
    Name = name;
}

In C#, it is as simple as the following:

public string Name {get; set;}

This is a property, which is nothing but a method that reads like a field. There are two types of properties: getters and setters. You can perform both read and write operations with them. From the preceding code, if you remove get, it will become write-only, and if you remove set, it will become read-only.

Internally, the property includes a setter and a getter method with a backing field. A backing field is simply a private field that stores a value, and getter and setter methods work with that value. You can write custom getters and setters as well, as follows:

private string _owner;
public string Owner
{
    get
    {
        return _owner;
    }
    set
    {
        _owner = value;
    }
}

In the preceding snippet, the Owner property shows what the default getter and setter methods would look like for the Dog class.

Just like other members, individual parts of a property (either getter or setter) can have their own access modifier, like the following:

public string Name {get; private set;}

In this case, the getter is public, and the setter is private. All parts of the property (getter, setter, or both, as defined) take the access modifier from the property (Name, in this case) unless explicitly specified otherwise (as in the case of private set). If you do not need to set a name, you can get rid of the setter. If you need a default value, you can write the code for this as follows:

public string Name {get;} = "unnamed";

This piece of code means that the Name field is read-only. You can set the name only through a constructor. Note that this is not the same as a private set because the latter means you can still change the name within the Dog class itself. If no setter is provided (as is the case here), you can set the value in only one place, the constructor.

What happens internally when you create a read-only property? The following code is generated by the compiler:

private readonly string _name;
public string get_Name()
{
    return _name;
}

This shows that getter and setter properties are simply methods with a backing field. It is important to note that, if you have a property called Name, the set_Name() and get_Name() methods will be reserved because that's what the compiler generates internally.

You may have noticed a new keyword in the previous snippet, readonly. It signifies that the value of a field can only be initialized once—either during declaration or in a constructor.

Returning a backing field with a property may seem redundant sometimes. For example, consider the next snippet:

private string _name;
 
public string Name
{
    get
    {
        return "Dog's name is " + _name;
    }
}

This code snippet is a custom property. When a getter or a setter is more than just a basic return, you can write the property in this way to add custom logic to it. This property, without affecting the original name of a dog, will prepend Dog's name is before returning the name. You can make this more concise using expression-bodied property syntax, as follows:

public string Name => "Dog's name is " + _name;

This code does the same thing as the previous code; the => operator indicates that it is a read-only property, and you return a value that is specified on the right side of the => operator.

How do you set the initial value if there is no setter? The answer to that is a constructor. In OOP, a constructor serves one purpose—that is, setting the initial values of fields. Using a constructor is great for preventing the creation of objects in an invalid state.

To add some validation to the Dog class, you can write the following code:

public Dog(string name)
{
  if(string.IsNullOrWhitespace(name))
  {
    throw new ArgumentNullException("name")
  }
  Name = name;
}

The code you have just written will prevent an empty name from being passed when creating a Dog instance.

It is worth mentioning that within a class, you have access to the object itself that will be created. It might sound confusing, but it should make sense with this example:

private readonly string name;
public Dog(string name)
{
  this.name = name;
}

The this keyword is most often used to clear the distinction between class members and arguments. this refers to the object that has just been created, hence, this.name refers to the name of that object and name refers to the passed parameter.

Creating an object of the Dog class, and setting the initial value of a name, can now be simplified as follows:

Dog ricky = new Dog("Ricky");
Dog sparky = new Dog("Sparky");

You still have a private setter, meaning the property that you have is not entirely read-only. You can still change the value of a name within the class itself. However, fixing that is quite easy; you can simply remove the setter and it will become truly read-only.

Note

You can find the code used for this example at http://packt.link/hjHRV.

Object Initialization

Often, a class has read and write properties. Usually, instead of setting the property values via a constructor, they are assigned after the creation of an object. However, in C# there is a better way—object initialization. This is where you create a new object and set the mutable (read and write) field values right away. If you had to create a new object of the Dog class and set the value of Owner for this object to Tobias, you could add the following code:

Dog dog = new Dog("Ricky");
dog.Owner = "Tobias";

This can be done using object initialization as follows:

Dog dog = new Dog("Ricky")
{
  Owner = "Tobias"
};

Setting initial properties like this when they are not a part of a constructor is generally more concise. The same applies to arrays and other collection types. Suppose you had two objects of the Dog class, as follows:

Dog ricky = new Dog("Ricky");
Dog sparky = new Dog("Sparky");

In such a case, one way of creating an array would be as follows:

Dog[] dogs = new Dog[2];
dogs[0] = ricky;
dogs[1] = sparky;

However, instead of this, you can just add the following code, which is more concise:

Dog[] dogs = {ricky, sparky};

In C# 10, you can simplify object initialization without providing the type, if it can be inferred from the declaration, as in the following code:

Dog dog = new("Dog");

Comparing Functions and Methods

Up until now, you might have seen the terms—function and method—used quite often, almost interchangeably. Now proceed to gain further insight into functions and methods. A function is a block of code that you can call using its name and some input. A method is a function that exists within a class.

However, in C#, you cannot have functions outside of a class. Therefore, in C#, every function is a method. Many languages, especially non-OOP languages, have only some functions that can be called methods (for example, JavaScript).

The behavior of a class is defined using methods. You have already defined some behavior for the Dog class, that is, getting its name. To finish implementing the behaviors for this class, you can implement some real-world parallels, such as sitting and barking. Both methods will be called from the outside:

public void Sit()
{
    // Implementation of how a dog sits
}
public void Bark()
{
    // Implementation of how a dog barks 
}

You can call both methods like this:

Ricky.Sit();
Sparky.Bark();

In most cases, it is preferable to avoid exposing data publicly, so you should only ever expose functions publicly. Here, you might be wondering, What about properties? Properties are just getter and setter functions; they work with data but aren't data themselves. You should avoid exposing data publicly directly, for the same reason you lock your doors, or carry your phone in a case. If data were public, everyone could access it without any restrictions.

Also, data should not change when the program requires it to be constant. A method is a mechanism that ensures that an object is not used in invalid ways, and if it is, it's well handled.

What if you need to validate the fields consistently throughout the application? Again, properties, that is, getter and setter methods, can help with this. You can limit what you can do with data and add validation logic to it. Properties help you be in full control of how you can get and set data. Properties are handy, but it's important to use them with discretion. If you want to do something complex, something that needs extra computing, it is preferable to use a method.

For example, imagine that you have a class for an inventory made up of items, each having some weight. Here, it might make sense to have a property to return the heaviest item. If you chose to do so through a property (call it MaxWeight), you might get unexpected results; getting the heaviest item would require iterating through a collection of all items and finding the maximum by weight. This process is not as fast as you would expect. In fact, in some cases, it might even throw an error. Properties should have simple logic, otherwise working with them might yield unexpected results. Therefore, when the need for compute-heavy properties arises, consider refactoring them to a method. In this case, you would refactor the MaxWeight property into the GetMaxWeight method.

Properties should be avoided for returning results of complex calculations, as calling a property could be expensive. Getting or setting the value of a field should be straightforward. If it becomes expensive, it should no longer be treated as property.

An Effective Class

The Dog class models a dog object; therefore, it can be called a model. Some developers prefer to have a strict separation between data and logic. Others try to put as much logic in a model as possible, so long as it is self-contained. There is no right or wrong way here. It all depends on the context you are working with.

Note

This discussion is outside the scope of this chapter, but if you would like to know more, you can refer to the discussion on Domain-Driven Design (DDD) at https://martinfowler.com/bliki/DomainDrivenDesign.html.

It is hard to pinpoint what an effective class looks like. However, when deciding whether a method fits better in class A or class B, try asking yourself these questions:

  • Would someone, who is not a programmer, know that you are talking about the class? Is it a logical representation of a real-world concept?
  • How many reasons does the class have to change? Is it just one or are there more reasons?
  • Is private data tightly related to public behavior?
  • How often does the class change?
  • How easy is it to break the code?
  • Does the class do something by itself?

High cohesion is a term used to describe a class that has all its members strongly related, not only semantically, but logically as well. In contrast, a low cohesion class has loosely related methods and fields that probably could have a better place. Such a class is inefficient because it changes for multiple reasons and you cannot expect to look for anything inside it, as it simply has no strong logical meaning.

For example, a part of a Computer class could look like this:

class Computer
{
    private readonly Key[] keys;
}

However, Computer and keys are not related at the same level. There could be another class that better suits the Key class, that is Keyboard:

class Computer
{
    private readonly Keyboard keyboard;
}
class Keyboard
{
    private readonly Key[] keys;
}

Note

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

A keyboard is directly related to keys, just as it is directly related to a computer. Here, both Keyboard and the Computer class have high cohesion because the dependencies have a stable logical place. You can now learn more about it through an exercise.

Exercise 2.02: Comparing the Area Occupied by Different Shapes

You have two sections of a backyard, one with circular tiles and the other with rectangular tiles. You would like to deconstruct one section of the backyard, but you are not sure which one it should be. Obviously, you want as little mess as possible and have decided to pick the section that occupies the least area.

Given two arrays, one for different sized rectangular tiles and the other for different-sized circular tiles, you need to find which section to deconstruct. This exercise aims to output the name of the section occupying less area, that is, rectangular or  circular.

Perform the following steps to do so:

  1. Create a Rectangle class as follows. It should have fields for width, height, and area:
    public class Rectangle
    {
        private readonly double _width;
        private readonly double _height;
        public double Area
        {
            get
            {
                return _width * _height;
            }
        } 
     
        public Rectangle(double width, double height)
        {
            _width = width;
            _height = height;
        }
    }

Here, _width and _height have been made immutable, using the readonly keyword. The type chosen is double because you will be performing math operations. The only property that is exposed publicly is Area. It will return a simple calculation: the product of width and height. The Rectangle is immutable, so all it needs is to be passed once through a constructor and it remains constant thereafter.

  1. Similarly, create a Circle class as follows:
    public class Circle
    {
        private readonly double _radius;
     
        public Circle(double radius)
        {
            _radius = radius;
        }
     
        public double Area
        {
            get { return Math.PI * _radius * _radius; }
        }
    }

The Circle class is similar to Rectangle class, except that instead of width and height, it has radius, and the Area calculation uses a different formula. The constant PI has been used, which can be accessed from the Math namespace.

  1. Create a Solution class with a skeleton method named Solve:
    public static class Solution
    {
        public const string Equal = "equal";
        public const string Rectangular = "rectangular";
        public const string Circular = "circular";
        public static string Solve(Rectangle[] rectangularSection, Circle[] circularSection)
        {
            var totalAreaOfRectangles = CalculateTotalAreaOfRectangles(rectangularSection);
            var totalAreaOfCircles = CalculateTotalAreaOfCircles(circularSection);
            return GetBigger(totalAreaOfRectangles, totalAreaOfCircles);
        }
    }

Here, the Solution class demonstrates how the code works. For now, there are three constants based on the requirements (which section is bigger? rectangular or circular, or are they equal?). Also, the flow will be to calculate the total area of rectangles, then of circles and finally return the bigger.

Before you can implement the solution, you must first create side methods for calculating the total area of the rectangular section, calculating the total area of the circular section, and comparing the two. You will do this over the next few steps.

  1. Inside Solution class, add a method to calculate the total area of the rectangular section:
    private static double CalculateTotalAreaOfRectangles(Rectangle[] rectangularSection)
    {
        double totalAreaOfRectangles = 0;
        foreach (var rectangle in rectangularSection)
        {
            totalAreaOfRectangles += rectangle.Area;
        }
     
        return totalAreaOfRectangles;
    }

This method goes through all the rectangles, gets the area of each, and adds it to the total sum.

  1. Similarly, add a method to calculate the total area of the circular section:
    private static double CalculateTotalAreaOfCircles(Circle[] circularSection)
    {
        double totalAreaOfCircles = 0;
        foreach (var circle in circularSection)
        {
            totalAreaOfCircles += circle.Area;
        }
     
        return totalAreaOfCircles;
    }
  2. Next, add a method to get the bigger area, as follows:
    private static string GetBigger(double totalAreaOfRectangles, double totalAreaOfCircles)
    {
        const double margin = 0.01;
        bool areAlmostEqual = Math.Abs(totalAreaOfRectangles - totalAreaOfCircles) <= margin;
        if (areAlmostEqual)
        {
            return Equal;
        }
        else if (totalAreaOfRectangles > totalAreaOfCircles)
        {
            return Rectangular;
        }
        else
        {
            return Circular;
        }
    }

This snippet contains the most interesting part. In most languages, numbers with a decimal point are not accurate. In fact, in most cases, if a and b are floats or doubles, it is likely that they will never be equal. Therefore, when comparing such numbers, you must consider precision.

In this code, you have defined the margin, to have an acceptable range of accuracy of your comparison for when the numbers are considered equal (for example, 0.001 and 0.0011 will be equal in this case since the margin is 0.01). After this, you can do a regular comparison and return the value for whichever section has the biggest area.

  1. Now, create the Main method, as follows:
    public static void Main()
    { 
        string compare1 = Solve(new Rectangle[0], new Circle[0]);
        string compare2 = Solve(new[] { new Rectangle(1, 5)}, new Circle[0]);
        string compare3 = Solve(new Rectangle[0], new[] { new Circle(1) });
        string compare4 = Solve(new []
        {
            new Rectangle(5.0, 2.1), 
            new Rectangle(3, 3), 
        }, new[]
        {
            new Circle(1),
            new Circle(10), 
        });
     
        Console.WriteLine($"compare1 is {compare1}, " +
                          $"compare2 is {compare2}, " +
                          $"compare3 is {compare3}, " +
                          $"compare4 is {compare4}.");
    }

Here, four sets of shapes are created for comparison. compare1 has two empty sections, meaning they should be equal. compare2 has a rectangle and no circles, so the rectangle is bigger. compare3 has a circle and no rectangle, so the circles are bigger. Finally, compare4 has both rectangles and circles, but the total area of the circles is bigger. You used string interpolation inside Console.WriteLine to print the results.

  1. Run the code. You should see the following being printed to the console:
    compare1 is equal, compare2 is rectangular, compare3 is circular, compare4 is circular.

    Note

    You can find the code used for this exercise at https://packt.link/tfDCw.

What if you did not have objects? What would the section be made of in that case? For a circle, it might be viable to just pass radii, but for rectangles, you would need to pass another collinear array with widths and heights.

Object-oriented code is great for grouping similar data and logic under one shell, that is, a class, and passing those class objects around. In this way, you can simplify complex logic through simple interaction with a class.

You will now know about the four pillars of OOP.

Visually different images
CONTINUE READING
83
Tech Concepts
36
Programming languages
73
Tech Tools
Icon Unlimited access to the largest independent learning library in tech of over 8,000 expert-authored tech books and videos.
Icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Icon 50+ new titles added per month and exclusive early access to books as they are being written.
The C# Workshop
notes
bookmark Notes and Bookmarks search Search in title playlist Add to playlist download Download options font-size Font size

Change the font size

margin-width Margin width

Change margin width

day-mode Day/Sepia/Night Modes

Change background colour

Close icon Search
Country selected

Close icon Your notes and bookmarks

Confirmation

Modal Close icon
claim successful

Buy this book with your credits?

Modal Close icon
Are you sure you want to buy this book with one of your credits?
Close
YES, BUY

Submit Your Feedback

Modal Close icon
Modal Close icon
Modal Close icon