C# Journey - 1. Introduction to C#
Embarking on the C# Journey: Exploring the Core Concepts - Part 1 of the Learning Series
Table of contents
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:
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.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.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.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.protected internal: A combination of
protected
andinternal
, theprotected 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:
Encapsulation: Properties allow you to encapsulate the internal state of an object, providing controlled access to its data.
Access Control: Properties can have different access levels (public, private, protected, etc.), allowing you to control how the data is accessed.
Validation: Properties can include validation logic, ensuring that the data is within acceptable bounds before setting it.
Lazy Initialization: Properties can be used for lazy initialization, where the value is computed or retrieved from another source only when needed.
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:
Simple Data Storage: Fields are used for straightforward data storage without additional logic or validation.
Performance: Direct field access is generally faster than property access, although the difference is usually negligible in most applications.
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:
set
: Theset
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.init
: Theinit
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, theinit
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 }