C# Journey -  1. Introduction to C#

C# Journey - 1. Introduction to C#

Embarking on the C# Journey: Exploring the Core Concepts - Part 1 of the Learning Series

Printing output to the console

In C#, interacting with the console is fundamental for displaying information and receiving input from the user. Let's explore how to effectively use Console.Write(), Console.WriteLine(), and Console.ReadLine(), along with some additional examples for working with different data types.

Using Console.Write()

When you use Console.Write(), the message is printed to the console without moving to a new line. This is similar to not including the newline character \n at the end of the message, as you might be familiar with from the C language.

Console.Write("This is ");
Console.Write("a single line.");

Using Console.WriteLine()

The Console.WriteLine() method prints a line to the console and automatically moves to the line below, making it easy to separate output.

Console.WriteLine("This is the first line.");
Console.WriteLine("This is the second line.");

Shortcut in Visual Studio: "cw"

Inside Visual Studio, you can quickly use the "cw" shortcut to generate a Console.WriteLine() statement, which can be a time-saver when writing code.

Reading User Input with Console.ReadLine()

To read user input, you can use Console.ReadLine(). However, it reads input as a string. If you want to accept other data types (like integers), you need to convert the input accordingly. Here's an example:

Console.Write("Enter an integer: ");
int a = Convert.ToInt32(Console.ReadLine());

Console.Write("Enter a double: ");
double b = Convert.ToDouble(Console.ReadLine());

By converting the input to the desired data type using Convert.ToInt32() and Convert.ToDouble(), you can handle various data types from user input.

In summary, these console output and input methods are essential tools in C# for communicating with the user and displaying program results. By understanding their nuances and handling different data types, you can create more interactive and robust applications.

Object-Oriented Programming (OOP), Core Concepts

OOP is based on three key principles:

Encapsulation

This involves hiding information outside a class, defining fields as private, and providing controlled access through functions (getters, setters, properties). It's like a restaurant with a waiter and a chef, each with their defined roles, just as classes in C# should have distinct responsibilities.

Inheritance

Inheritance allows classes to share attributes. There are superclasses that pass attributes to subclasses using the : notation. We use "virtual" and "override" to modify behavior in subclasses.

Polymorphism

Polymorphism allows us to treat different subclasses as part of a broader superclass. For instance, if we have a Car, Bicycle, and Motorcycle, all being subclasses of a Vehicle, we can use polymorphism to handle them uniformly by using an array of Vehicle objects, such as Vehicle[] vehicles.

Summary

By adhering to these principles, OOP helps us encapsulate object details, utilize inheritance for code reuse, and achieve polymorphism for handling diverse objects more effectively.

Clasess

In the world of programming, classes serve as the architectural blueprints from which we construct real, tangible objects. They provide the essential structure needed to create custom data types, expanding the capabilities beyond the basic built-in data types that are ubiquitous across programming languages. Let's explore this concept further, uncovering the power of classes and how they enable us to encapsulate complex entities.

Built-in Data Types

In every programming language, you'll find fundamental data types that form the backbone of data manipulation. These core types include:

  • int: Representing integers

  • string: Storing sequences of characters

  • char: Holding individual characters

  • double: Enabling high-precision floating-point numbers

  • float: Handling floating-point numbers (with less precision than double)

  • decimal: Ensuring precision in financial calculations

  • bool: Expressing Boolean values (true or false)

While these built-in types are essential for many tasks, they may not suffice for representing more complex real-world entities. Imagine trying to encapsulate the diverse attributes of a "car" using only these basic types, it's impossible! This is where classes come into play.

Custom Data Types

When the built-in data types fall short, we create our own custom data types using classes. Think of classes as a meticulously designed blueprint. They consist of fields, methods, properties, and other members that define the structure and behavior of a specific data entity. In the case of a "car," we would design a class with attributes such as "make," "model," "year," "color," and more.

class Car
{
    public string make;
    public string model;
    public int year;
    public string color;
}

These class blueprints allow us to instantiate actual objects. Each object, created from a class, is a concrete instance of that data type. If we think of the class as the blueprint, the object is the real, tangible thing we create based on that blueprint.

Reusability

Once you've designed a well-crafted class for one purpose, you can reuse it across your application or even in other projects, avoiding redundant code and fostering efficient development.

Objects

From the Microsoft documentation:

A class or struct definition is like a blueprint that specifies what the type can do. An object is basically a block of memory that has been allocated and configured according to the blueprint.

class Program
{
    static void Main()
    {
        // person1 is object
        Person person1 = new Person("Filip", 20);
    }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
    // Other properties, methods, events...
}

Access Modifiers

In C#, access modifiers are crucial tools for maintaining control over your application's functionality and ensuring security. These modifiers help you define the accessibility of classes, members, and other elements within your codebase. Let's explore the five primary access modifiers:

  1. public: The public modifier grants unrestricted access to the designated element. It can be accessed from any part of your application, including other classes, namespaces, or external assemblies.

  2. private: With the private modifier, you restrict access to within the same class. This encapsulates the member, preventing it from being directly accessed or modified from outside the class.

  3. internal: The internal modifier allows access only within the same assembly (project or assembly produced by the same compiler). It's useful for creating elements that should be hidden from external code while being accessible within a cohesive unit.

  4. protected: Using the protected modifier, access is limited to the same class and any derived classes (subclasses) that extend it. This promotes inheritance and facilitates the creation of more specialized classes while maintaining controlled access to the base class's members.

  5. protected internal: A combination of protected and internal, the protected internal modifier grants access to the same assembly and derived classes, whether they are in the same assembly or another assembly.

C# Properties, Getters and Setters

In C#, properties are powerful elements that enable you to control how values are accessed and modified within a class.

The Anatomy of a Property

A property in C# is a member of a class that acts as a bridge between the outside world and the internal data of an object. It typically consists of two parts: a get accessor and a set accessor. The get accessor retrieves the value of the property, while the set accessor allows you to assign a new value to the property.

Here's a basic example of a property within a C# class:

public class Car
{
    private string _make;

    public string Make
    {
        get { return _make; }
        // In our example "Toyota" is passed as a value
        set { _make = value; }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Car myCar = new Car();
        myCar.Make = "Toyota"; // Setting the Make property
        Console.WriteLine(myCar.Make); // Getting the Make property
    }
}

In this example, we have a Car class with a property named Make. The get accessor retrieves the value of the private field _make, and the set accessor allows external code to modify the value of _make.

Shortcut in Visual Studio: "propfull"

Inside Visual Studio, you can quickly use the "propfull" shortcut to generate a property, which can be a time-saver when writing code.

Should u use properties, or fields inside a class?

What are fields and properties?

//Book class
public string title; //field
private string ptitle;
public string Ptitle //property
{ 
    get { return ptitle; }
    set { ptitle = value; }
}
static void Main(string[] args)
{
    book.Title = "Harry Potter"; //don't forget to use capital T in the Title
    Console.WriteLine(book.Title);
}

In C#, properties and fields serve different purposes, and their usage depends on the specific requirements of your class design. Here's a brief overview:

Properties:

  1. Encapsulation: Properties allow you to encapsulate the internal state of an object, providing controlled access to its data.

  2. Access Control: Properties can have different access levels (public, private, protected, etc.), allowing you to control how the data is accessed.

  3. Validation: Properties can include validation logic, ensuring that the data is within acceptable bounds before setting it.

  4. Lazy Initialization: Properties can be used for lazy initialization, where the value is computed or retrieved from another source only when needed.

  5. Changing Implementation: If you start with a simple field and later need to add validation or change the internal representation, you can do so without affecting the external usage if you use a property.

Fields:

  1. Simple Data Storage: Fields are used for straightforward data storage without additional logic or validation.

  2. Performance: Direct field access is generally faster than property access, although the difference is usually negligible in most applications.

  3. Public Fields: In some cases, you might have public fields for simple data structures (such as simple data transfer objects) where encapsulation isn't a concern.

So, when should you use fields? Use fields when you need a simple data storage mechanism and when encapsulation, validation, or other property features are unnecessary or would not add significant value. Public fields should be used with caution to avoid breaking encapsulation.

In most cases, it's a good practice to use properties, as they provide more control over how the data is accessed, and they allow you to maintain encapsulation and easily change the internal implementation if needed in the future. However, if you find that you have a specific need for a simple data storage without additional logic, fields can be appropriate.

Constructor

A constructor is a vital component in object-oriented programming, invoked automatically every time we create a new instance of a class. In Visual Studio, the "ctor" shortcut is a handy abbreviation, enabling you to swiftly generate constructor code, expediting the object initialization process. Here's a real-world example of this concept:

namespace exercise
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Book book = new Book();
            Book book2 = new Book();
            Book book3 = new Book();
        }
    }
}
namespace exercise
{
    internal class Book
    {
        private static int counter = 0;
        public int price;
        public Book()
        {
            counter++;
            Console.WriteLine($"knjiga {counter} napravljena");
        }
    }
}
//console
book 1 made
book 2 made
book 3 made

In this example, we have a Book class with a constructor that increments a static counter each time a new Book object is created. This counter keeps track of the number of books created, and the constructor displays a message indicating the creation of each book. By utilizing constructors, we can ensure proper initialization of objects and perform any necessary setup when creating instances of a class.

The "this" Keyword

When creating a new object using constructor arguments (as in Book book = new Book("Harry Potter")) instead of setting the title in the Main method with book.title = "Harry Potter", we employ the "this" keyword within the Book class to handle this scenario, when the names of the field and the argument are the same.

namespace Exercise
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Book book = new Book("Harry Potter");
            Console.WriteLine("Book title: " + book.title);
        }
    }
}

namespace Exercise
{
    internal class Book
    {
        public string title;

        public Book(string title)
        {
            this.title = title;
        }
    }
}

// Console
Book title: Harry Potter

This technique allows us to set the title property directly when creating the object. By using the "this" keyword, we differentiate between the class field title and the constructor parameter (argument) title. This ensures that the proper value is assigned to the class member, as shown in the above example where the book title "Harry Potter" is directly set during object creation.

Interface

What is an Interface?

An interface acts as a blueprint, defining the structure for classes that inherit from it, without providing concrete implementations. Think of it as a contract that enforces certain behaviors.

Interfaces offer a more flexible approach. A class can inherit from another class, but a class can inherit from multiple interfaces.

Here's a practical example:

class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Dog();

        myDog.Hunt(); // Dog is a predator
        myDog.Run(); // Dog is also a prey
    }
}
interface IPredator
{
    void Hunt();
}

interface IPrey
{
    void Run();
}

class Dog : IPredator, IPrey
{
    public void Hunt()
    {
        Console.WriteLine("Dog hunts.");
    }

    public void Run()
    {
        Console.WriteLine("Dog runs.");
    }
}

"object" keyword

In C#, the "object" keyword is a reference type that represents the base type for all other data types. This means that any value in C# can be implicitly converted to the "object" type. This versatility is particularly useful when you want to work with different data types within a single container, such as a collection or method parameter.

C# Records

When starting a new project, you might encounter an issue related to the "record" keyword. A quick fix for this is to change the project to target .NET 5.0, and once you save the changes, the error should disappear.

Records in C# are more than just fancy classes, they bring a wealth of behind-the-scenes code and behavior. They exhibit behavior similar to value types, making them convenient for equality checks. When comparing two records with the same values using the Equals method, you'll get a true result. This contrasts with classes.

One notable characteristic of records is their immutability. Once you assign values to a record, they are, by default, unchangeable. While it's possible to force changes to the values, doing so undermines the concept of records and negates some of the benefits they offer.

One of the key distinctions between classes and records lies in their behavior when it comes to displaying information.

Consider the scenario where we attempt to display a class instance and a record instance using Console.WriteLine. With a class, what we see is typically the fully qualified type name, including the namespace. This output can be informative, especially in larger projects, as it clearly indicates the origin of the class, but it doesn't directly reveal the instance's specific data.

Difference between "init" and "set"

In C#, init and set are both used to assign values to properties, but they have different purposes and behaviors:

  1. set: The set keyword is used in property setters to allow you to assign a value to a property. This value can be modified at any time after the object is constructed, and there are no restrictions on when or how it can be changed.

  2. init: The init keyword is used in property setters to indicate that the property can only be assigned a value during object initialization, typically in the object's constructor or object initializer. After the object is initialized, the init property cannot be changed.

class Person
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

Here's a comparison of the two:

Using set:

csharpCopy codeclass Person
{
    private string _name;

    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

With the above code, you can change the Name property at any time after the object is created:

Person person = new Person();
person.Name = "John"; // This is allowed
person.Name = "Jane"; // This is also allowed

Using init:

class Person
{
    public string Name { get; init; }
}

With the init keyword, you can only set the Name property during object initialization:

Person person = new Person { Name = "John" }; // This is allowed
person.Name = "Jane"; // This will result in a compile-time error after object initialization

In this example, attempting to change the Name property after the object is initialized using the init keyword will result in a compile-time error, providing a more strict form of immutability.

The init keyword is particularly useful when you want to ensure that certain properties of an object can only be set during object creation and remain unchanged afterward, providing an additional level of safety and preventing unintended modifications after initialization.

Nondestructive mutation for anonimus types

C# 9 introduced the with keyword, it also works in C# 10:

var a1 = new { A=1, B=2, C=3, E=4 }

var a2 = a1 with { E=5 } // { A=1, B=2. C=3, E=5 }