Laboratory Training 1

OOP Bases. The Base Syntax of C# Programming Language

1 Training Tasks

1.1 Individual Task

Create a C# console application that implements classes according to the task in the table below:

Group of Entities Entity Additional Class
Group Student Address
Institute Faculty Dean
Faculty Specialty Dean
Examinations Subject Lecturer
City Ward Country
Region City Country
Sports group Member Address
Football team Player Country
Group Singer Country
Group Album Country
Album Song Author of Lyrics
Flat Room Address
Storybook Story Author
Artist Masterpiece Country
Writer Novel Country

This list can be extended with permission of the lecturer.

The field of the auxiliary class type must be located in one of the classes (groups of entities or entity) depending on the task variant. In a class that represents a group of entities, provide functions for finding data on two different grounds. Classes must contain public properties.

Data to be looked for would be entered from the keyboard at runtime. Test data should be prepared so that the search gave more than one result. Classes should contain private data and public properties.

1.2 Use of Nullable Types

Develop a program, which defines the functions that calculates the square root (Math.Sqrt()) of its argument. This function should return null, if it is impossible to calculate the root, or square root of an argument. Implement testing in Main() function.

Implement the function of calculating the square root with parameters of type double (argument x) and out int (error code), which returns double? with the value of the square root (or null if the square root cannot be found). The error code after the function call must be 0 (if the square root calculation was successful), or –1 if the argument is negative. Test the function for different argument values.

The algorithm for calculating the square root is to determine the initial approximation (for example, 1) and sequentially obtain new approximations as the arithmetic mean of the previous number and the argument divided by this approximation. The algorithm ends when two consecutive approximations differ less than the specified accuracy.

Note: do not use Math.Sqrt().

1.3 Greatest Common Divisor

Create a console application in the Main() function, which has a static local function (with the result type void) for calculation of the greatest common divisor of two positive integer numbers by the simplest Euclidean algorithm. Numbers must be passed as parameters with the ref attribute. As a result of the function, the initial values must be replaced by the value of the greatest common divisor. You should call the created local function for different arguments.

1.4 Class for Representation of a Quadratic Equation (Advanced Task)

Create a class "Quadratic", whose roots can be obtained as read-only properties of nullable type. Add indexer to access the roots by index.

1.5 Working with Jagged Array

Develop a program that is defined and initializes two-dimensional array of integers, and create jagged array, which lines contain even elements of the first array.

2 Instructions

2.1 OOP Bases

2.1.1 Programming paradigms

A programming paradigm is a set of ideas and concepts that define the style of writing computer programs. It is a way of conceptualizing that defines the organization of computation and the structuring of the work performed by a computer.

The original approach to programming was not based on any methodology. The program consisted of sequentially executed commands, as well as labels, conditional and unconditional jumps. Modern programming methodology includes a large number of paradigms, the most essential of which are the following:

  • Imperative programming describes the process of obtaining results as a sequence of instructions to change the state of the program.
  • Functional programming views a computation as a sequence of function calls without saving the state of the application.
  • Structured programming defines the program as a set of blocks.
  • Procedural programming is a functional stateful approach.
  • Modular programming involves the division of a program into independent logical and physical parts that can be independently processed.
  • Component-based programming involves maintaining the modular structure of the software while executing the program.
  • Object-oriented programming (OOP, object-oriented programming) organizes a program as a collection of objects (data structures consisting of data fields and methods), as well as their interaction.
  • Prototype programming (prototype-based programming) is a kind of object-oriented programming, implemented not through classes, but through the inheritance of objects by cloning an existing instance of an object (prototype).
  • Generic programming is a description of data and algorithms that can be applied to different types of data without changing this description.
  • Event-driven programming assumes that computational control is defined through events (asynchronous input and messages from other programs or threads, etc.).
  • Metaprogramming involves creating programs that produce other programs as a result of their work, or programs that change themselves at runtime.
  • Declarative programming defines the logic of a computation without describing the flow of control.

There are also many other programming paradigms: logic, aspect-oriented, agent-oriented, etc.

2.1.2 Methodology of imperative programming

Imperative programming is a programming paradigm that describes the process of obtaining results as a sequence of instructions for changing the state of the program. More often, imperative programming, in which the necessary sequence of actions is determined, is contrasted with declarative programming, which involves determining what we want to receive. Unlike functional programming, the imperative paradigm assumes the presence of state, which can be stored, for example, using global variables.

In addition to the original (non-structural) approach, imperative programming includes procedural and modular programming. In addition, within the object-oriented methodology, the imperative approach is used to implement class methods.

To implement a "non-structural" approach in a programming language, the following tools are required:

  • description of variables;
  • sequential execution of statements, in particular, assigning certain values to variables;
  • labels;
  • unconditional jump (goto);
  • conditional jump (if...goto).

In the C# language, this approach is implemented through appropriate syntax constructs.

2.1.3 Implementation of the Structural Approach

Structured programming is a paradigm that involves writing a program as a set of blocks. Such blocks are branches, cycles, a sequence of statements. Due to the presence of cycles with precondition, postcondition and with a parameter, the program can be fully implemented without conditional and unconditional jumps.

Modern structured programming languages support a separate scope of blocks ( can be created inside blocks).

The implementation of structured programming is based on the use of the following constructs:

  • sequential execution (similar to non-structured programming);
  • branching: conditional statement (if, if...else) and switch (switch)
  • cycles: with precondition (while), with postcondition (do...while), with parameter (for)
  • programming block – one or more statements enclosed in special brackets (e.g.{ and }); the block defines its scope; inside the block you can describe local variables, constants, types, etc.; blocks can be nested one inside the other.

All necessary syntax constructs are present in the C# language.

2.1.4 Implementation of the Procedural Approach

The implementation of procedural programming assumes the presence the concept of a subroutine (procedure, function) that defines its own scope and produces a certain result, as well as means of calling functions with the subsequent use of this result. When a subroutine is called, flow control is transferred from the point of invocation to the code of subroutine, and then returns to the point of invocation, and subsequent instructions are executed.

To place the data of individual subroutines (functions), the so-called call stack is organized in the computer memory allocated for the application. The call stack stores the information necessary to return flow of control from subroutines to the calling subroutine (the main program, in particular). In addition to addresses, the call stack can store subroutine arguments, local variables, and other temporary data.

In the C# language, the procedural approach is implemented through the use of static functions that are in the scope of classes.

2.1.5 Implementing a Modular Approach

Modular programming involves the division of program code into separate modules containing logically related elements (types, data, subroutines). At a logical level, languages support so-called namespaces. A namespace is a named part of the global scope that can contain declarations and definitions. Namespaces help avoid name conflicts.

At the physical level, modules can be separate source files, libraries, assemblies, object modules, etc. (depending on the programming language and software platform).

To implement a modular approach in C#, namespaces are used for logical grouping. The so-called assemblies provide physical grouping.

2.1.6 Origins and Benefits of Object-Oriented Approach

In the 1970s, the software development industry faced challenges due to a significant increase in the complexity of software systems. The appearance of dialogue systems with complex behavioral mechanisms led to the emergence of problems that could not be solved by the traditional procedural way. The possibility of asynchronous data entry was not consistent with the concept of data-driven programming.

The software is inherently very complex. The complexity of software systems often exceeds human intellectual potential. According to one of the founders of object-oriented methodology, Grady Booch, this complexity comes from four elements:

  • the complexity of domain;
  • the complexity of managing the development process;
  • the difficulty of ensuring software flexibility;
  • the complexity of the behavior of discrete systems.

We can overcome these problems with decomposition, abstraction and hierarchy. Instead of functional decomposition, on which procedural programming is built, the object-oriented paradigm offers object decomposition. In addition, the concept of classes allows you to provide the necessary level of data abstraction and hierarchical representation of objects.

The terms "objects" and "object-oriented" in the modern sense of object-oriented programming first appeared in the research of the artificial intelligence group at the Massachusetts Institute of Technology in the late 1950s and early 1960s. The concepts "object" and "instance" appeared in a glossary developed by Ivan Sutherland in 1961 and are associated with the description of a sketchpad.

The first language of object-oriented programming was Simula 67. This language was developed for discrete simulation. Language was created in Norway in 1967. The authors of this language were Ole-Johan Dahl and Kristen Nygaard.

The first universal object-oriented language was Smalltalk. It's widely widespread version is Smalltalk 80. The authors of this language were Alan Kay and Dan Ingalls

2.1.7 Components of an Object-Oriented Methodology

The main components of the object-oriented methodology are object-oriented analysis, object-oriented design and object-oriented programming.

Object-oriented analysis involves the creation of an object-oriented model of the subject area. This is not about designing software classes, but about using the concepts of object-oriented methodology to represent a real system.

Object-oriented design is the process of describing classes of future software using formal methods (typically graphical), as well as determining the interaction of classes and objects. Separating the design process from direct coding is needed to overcome the complexity of the software by controlling the relationships between individual entities and allows you to create software that allows for collaborative development and code reuse. The efficiency of the design process is increased through the use of design patterns.

Object-oriented programming is one of the programming paradigms and involves the direct creation of classes and objects, as well as the definition of relationships between them, performed using some object-oriented programming language.

2.1.8 Basic Principles and Concepts of the Object-Oriented Paradigm

The basic principle of the object-oriented approach is data abstraction. Abstraction involves the use of only those characteristics of an object that are sufficient to represent it in the system and distinguish it from all other objects. The main idea is to separate the way complex objects are used from the details of their implementation. This approach is the basis of object-oriented programming.

This principle is implemented through the concept of a class. A class is a structured data type, a set of data members of different types and functions for working with this data. An object is an instance of a class.

Object data (fields, sometimes data members) are variables that describe the state of the object.

Object functions (methods) are functions that have direct access to object data. Sometimes it is said that methods determine the behavior of an object. Unlike normal (global) functions, it is necessary to first create an object and call a method in the context of this object.

Objects are characterized by a life cycle. Creating objects involves calling a special data initialization function, the so-called constructor. Constructors are called directly after creating an object in memory. Most object-oriented programming languages ​​support mechanisms for releasing resources involved in the life cycle of objects using destructors. A destructor is a special function that is called immediately before deleting an object and frees system resources that were involved in the process of creating and operating the object.

It is possible to define three main concepts underlying object-oriented programming. These are encapsulation, inheritance and polymorphism.

Encapsulation (data hiding) is one of the three fundamental concepts of object-oriented programming. The content of encapsulation consists in hiding the details of the object's implementation from the external user. Data (fields) are accessed through open access functions or properties.

Inheritance is a mechanism for creating derived classes from base classes. Creating a productive class involves extending it by adding new fields (attributes) and methods. C++ has so-called private and protected inheritance. These forms of inheritance allow you to restrict access to elements of the base classes of the outer class. In most object-oriented programming languages, only open inheritance is supported: elements retain their visibility during inheritance. In this case, closed elements are inherited, but become unavailable for direct access in production classes.

Polymorphism is a mechanism for determining among functions with the same function name to call, based on the type of parameters (compile-time polymorphism) or the object for which the method is called (run-time polymorphism).

Runtime polymorphism is a property of classes that allows the behavior of objects to be determined at compile time and at run time. Classes that declare an identical set of functions, but are implemented for specific requirements, are called polymorphic classes.

Connecting the function body to the point of its invocation is called linking . If it occurs before the start of program execution, it is called early binding. This type of communication is present in procedural languages ​​such as C or Pascal. Late binding means that binding occurs at runtime and, in object-oriented languages, depends on object types. Late binding is also called dynamic, or runtime binding. A late connection mechanism is used to implement polymorphism.

In object-oriented programming languages, late binding is implemented through the mechanism of virtual functions. A virtual function (virtual method) is a function defined in a base class and overridden in derivatives, so that the specific implementation of the calling function will be determined during program execution. The choice of implementation of the virtual function depends on the real (and not defined during the definition of the pointer or reference) type of the object. Thus, the behavior of previously created classes can be changed later by overriding virtual methods. In fact, classes that contain virtual functions are polymorphic.

Closely related to the object-oriented paradigm is the concept of event-driven programming, in which the general organization of the program involves the creation and registration of objects, followed by the reception and processing of asynchronous events and the exchange of messages between objects.

2.2 .NET Platform and C# Programming Language

2.2.1 Common concepts of .NET Platform

A .NET platform is a software environment that provides program development and running, based on the common infrastructure: a common runtime environment and a common set of types. The .NET platform, like Java platform, allows you to create programs that can be executed under different operating systems without recompiling. The .NET architecture is based on a set of standards of WWW consortium, primarily refers to the HTTP protocol (it is a basic protocol for Web-services technology) and XML language.

Compared with previous technologies that suggested Microsoft, .NET has the following advantages:

  • managed code;
  • common class library built on common class hierarchy;
  • ability to design self-describing components that do not require external registration.

A .NET Framework was an implementation of the .NET platform developed by Microsoft Corp. There are several versions of .NET Framework. .NET Framework 3.5 is automatically installed when you install Windows 7. Programs of this course focused on work with .NET Framework 4.0 (comes with Windows 10).

In parallel with the development of new versions of the .NET Framework, Microsoft has published a modular open source platform .NET Core for Windows, Linux and macOS. There are several versions (up to 3.1 inclusive). New versions of the .NET Framework (up to and including 4.8) were released in parallel. The branches differed not only in licensing terms, but also in the set of technologies they supported. In 2020, Microsoft reunited two branches in the .NET 5 platform.

In 2021, the version of .NET 6 was published. Long-term support for this version (until November 2024) is planned. In November 2022, version .NET 7 was released.

The last release of .NET platform can be downloaded from https://dotnet.microsoft.com/download. You should choose version for your operating system.

.NET is based on two components:

  • runtime environment – Common Language Runtime (CLR);
  • Common Type System (CTS).

CLR is a fundamental part of the .NET architecture. CLR provides memory management, working with threads, exception handling, garbage collection, and security. CLR allows program development based on different programming languages. Programming code, which can be managed by CLR, is called managed code in contrast to unmanaged code.

Compiling of source code written in high-level programming language includes two stages:

  • compiling of source code into so called Common Intermediate Language (CIL).
  • compiling of intermediate code into instructions of particular computer

On the second stage, technology of just-in-time compiling (JI) is used. Compiling on this stage takes place first time and each time when project parts are modified.

2.2.2 Common Type System

Different parts of program written using different programming languages use so called Common Type System (CTS). Type system provides value types and reference types.

Value types directly contain their data. Instances of value types are either allocated on the stack or allocated inline in a structure. Value types can be built-in (implemented by the runtime), user-defined, or enumerations.

Reference types contain addresses of data. Memory in which data will be stored is allocated in so called managed heap. There are three groups of reference types:

  • self-describing types (i. e. class types)
  • pointer types
  • interface types

Class types can be divided into user classes, boxed value types, and delegates.

All .NET types build common hierarchy with common ancestor: System.Object.

A class library is a set of classes and interfaces that you can use to develop applications with different architectures. For instance, if you want to develop Windows applications, Windows Forms library is used. Web Forms library provides server-level components.

2.2.3 Assemblies

Assembly is a set of types (with their implementation) and recourses which are developed for collaboration inside of particular application or library.

Assembly contains code, which is executed by CLR. Assembly consists of one or more files, one of which must contain the manifest, which has the metadata for the assembly. Metadata contains information about the resources, which are exported by assembly.

Assemblies are version control units. They are also application deployment units.

Assemblies can by dynamic (created at runtime) or static (stored in one or several files). For example, static assembly can be created from a single source file and multiple file resources. Dynamic assemblies are created in memory and executed.

CLR creates so called Global Assembly Cache (GAC), which contains different versions of assemblies used by different applications.

2.2.4 Key Features of C# Programming Language

Though possibility of use different programming languages, C# is the most convenient high-level language that can be used for development of all kinds of .NET applications.

C# absorbed better from languages such as C++, Visual Basic, Java and Object Pascal. There are key features of C# programming language:

  • support for object-oriented programming model
  • built-in support of CTS
  • strict typing
  • support for properties and events
  • support for operator overloading and indexers
  • automatic garbage collection
  • provision of opportunities for use pointers (in unmanaged code)
  • use of attributes

The latest version of language is C# 10.0 (supported by Visual Studio 2022). For examples and tasks of the course, version 9.0 is sufficient (supported by Visual Studio 2019 Preview).

Unlike C++, C# does not support global variables and functions. This is done to prevent name conflicts. The program consists of one or more descriptions of types (classes, interfaces, structures, enumerations, and delegates). Types are combined into so-called namespaces.

The most widespread data types are classes. Their definition consists of fields, methods, properties and other elements. One of the classes that make up the application should specify a static Main() method, which determines program's starting point.

The program may consist of several files. Like C++, C# does not require matching file names with the names of classes. Information about the files and the types defined within them is contained in the metadata of the assembly.

2.3 Creation of Console C# Application in Visual Studio Programming Environment

To work with the latest version of C#, you should download the latest version of Visual Studio. After downloading installer and starting it, you should choice whether particular components you need to install. Components are joined into groups, so-called Workloads. In our case, it is enough to choose the following workload: .NET desktop development. Then you can press Install button. After installation, you should register using Microsoft account. This account can be created free.

By default, after loading Visual Studio you can show a start page. The Get Started panel contains a list of recommended activities. You can create a new project directly. You can also use Continue without code option. An empty environment window opens. Now a new project can be created in several ways:

  • through the main menu (File | New | Project...);
  • by clicking New Project button on the Standard toolbar (if this button was added before);
  • using keyboard shortcut Ctrl+Shift+N.

You can also create a new project from the start page directly. This is the simplest way (without opening an empty environment). Assume that you started with creation of a new project using one of listed ways. Now a new popup window called Create a new project appears on the screen. You need to choose necessary template for our project. In the right-hand pane, choose C# Console Application. Then the next popup window Configure your new project appears. You can change the project name (Name), the folder in which the solution will be located (Location) and the name of the solution (Solution name). A solution is a conceptual container of a project or group of logically related projects that share common properties and settings. Project includes a set of source files and associated metadata such as references and assembly instructions. The project usually produces one or more binary files as a result of compilation. If the solution involves the creation of an application, one (and only one) of the projects can be labeled as a startup project.

The Place solution and project in the same directory checkbox can be checked for small projects. But if we need to add several projects into the common solution, this checkbox should not be checked. In our case, the project name will be set to Hello, and the Solution can be renamed to Labs. After you click Next, on Additional information page you should choose the Target Framework: .NET 7.0. You should also check the Do not use top-level statements option. After you press Create button, Visual Studio automatically generates a file called Program.cs in the subdirectory Hello of Labs folder. The text is as follows:

namespace Hello
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

The source code contains a description of its own namespace called Hello (same name as the project name), and the definition of a new class (Program) with static Main() method, which determines program's starting point. Now program can be started by selecting Debug | Start Without Debugging function of main menu. You also can press Ctrl+F5. As expected, the console window displays appropriate text.

You can write a program without arguments of Main() function:

namespace Hello
{
    internal class Program
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
        }
    }
}    

Sometimes you want to get so called exit code that program returns the operating system. A value of 0 (zero) denotes a successful completion, other integer numbers can be interpreted as errors. The Main() function can return an integer value (int) instead of void:

namespace Hello
{
    internal class Program
    {
        static int Main()
        {
            Console.WriteLine("Hello World!");
            return 0;
        }
    }
}

The Main() function, as well as Program class, can be declared as public.

The C# 9 and later versions allow you to create very simple "Hello World" program. It will contain the only line of code. By creation of such program, Do not use top-level statements option must be unchecked:

System.Console.WriteLine("Hello World!");

You should keep in mind that some invisible class with the static Main() method is created automatically. The file with such source code can be the only in a project.

Visual Studio environment provides convenient means of debugging programs. You can add a breakpoint (F9) at the desired line of code. After starting debugging (Debug | Start Debugging F5), the program will be loaded for execution, but its execution will be paused at the point of interruption. The appearance and location of windows are somewhat changed. The Autos and Locals tabs display intermediate values of variables. To terminate program, use Stop Debugging function (Shift + F5).

2.4 Bases of C# Language Syntax

2.4.1 Preprocessor Directives

The C# compiler does not use preprocessing. Nonetheless, C# includes a set of preprocessor directives. Directive always starts from # character followed by a directive name. Directive should be located on a separate line, the C++ style comments (//) also allowed. The possible directives can be used for

  • conditional compiling (#define, #undef, #if, #elif, #else, #endif)
  • generation of warnings and error (#warning and #error)
  • definition of outline view (#region and #endregion).

The last pair of directives allows creation of named piece of source code that can be expanded and collapsed using means of hierarchic representation (outlines) in Visual Studio.

Directives do not allow you to create macros or include header files. Generally, mechanism of header files does not supported.

2.4.2 Comments

All programming languages support the concept of comments. Comment is some text inside the source code that is not processed by compiler. C# supports three kinds of comments:

  • C-style comments (/* */)
  • C++-style comments (//)
  • XML comments (///).

XML comments allow generation of documentation. The idea of documentation generation is similar to Java, but instead of the HTML-document, XML-document can be generated. This document, in turn, can be used to produce standard documentation files (Help).

2.4.3 Identifiers, Keywords, and Reserved Words

The source code consists of tokens. A token is a sequence of characters that have a certain common meaning. Tokens are separated by spaces, tabs, newline characters, etc. These are so called separators. Tokens are divided into the following groups:

  • keywords (reserved words)
  • identifiers
  • literals (constants)
  • operators.

Such as С++, C# is case sensitive. Characters are represented using Unicode standard.

Keywords are predefined reserved names that have special meaning to the compiler. They cannot be used as identifiers in the program. Examples of such words are int, double, if, for, class, struct, etc. In addition to 79 reserved keywords, C# provides so called context-sensitive keywords. These words are not reserved and acquire the status of keywords only in a particular context. Examples of context-sensitive keywords are set, get, var, value, etc.

Identifiers are used to name types, variables, functions, and other program objects. The first character must be a letter or an underscore character ("_"). The second and the other characters may be also digits. Use the underscore character is not recommended.

Rules of building identifier names are the same as ones in C++. It is recommended to use meaningful names that reflect the nature of objects or functions. You cannot use spaces within identifiers. So if you want to create an identifier from a few words, those words written without spaces, starting second, third and other words with a capital letter. For example, you can create a variable name:

thisIsMyVariable

Names of namespaces, types (classes, interfaces, lists, structures, and delegates), methods, public fields and properties are started with a capital letter.

Local variables are defined within methods. Description of local variables is similar to and C++. For example:

int i = 11;
double d = 0, x;
float f; 
int j, k;

Local variables can be defined anywhere within the function body, as well as within inner blocks. In C#, you cannot define inner names, which were also defined in the outer block:

{
    int i = 0;
    {
        int j = 1; // Variable j is defined in the inner block
        int i = 2; // Error! Variable is defined in the outer block
    }
}

Unlike C++, you cannot declare variables without their definition.

You can use the constants (literals) within expressions. Examples of such literals are: 12, 0x22, 3.1416, 'k', or "some text". It is often advisable to create so-called named constants. To do this, use const keyword, which is applied to variable names means that they cannot be changed, for example:

const int h = 0; 
const double pi = 3.14169265;

2.4.4 Data Types

Each variable or constant has its own type. The C# types correspond to ones in CTS. C# provides aliases for CTS types. C# allows you to use both signed and unsigned integer types. The following table shows CTS types and their synonyms that are used in C#.

CTS Type C# Type Description
System.Object object Common base type
System.String string String type
System.SByte sbyte One-byte signed integer
System.Byte byte One-byte unsigned integer
System.Int16 short Two bytes signed integer
System.UInt16 ushort Two bytes unsigned integer
System.Int32 int Four bytes signed integer
System.UInt32 uint Four bytes signed integer
System.Int64 long Eight bytes signed integer
System.UInt64 ulong Eight bytes unsigned integer
System.Char char Unicode character
System.Single float Floating point real value
System.Double double Double precision floating point real value
System.Boolean bool Logical value (true and false)
System.Decimal decimal Extra precision real value (16 bytes)

Types object and string are reference types, all the rest are value types.

In C# 9.0, new integer types have been added: nint (signed) and nuint (unsigned). The actual size of these types is determined already at runtime and depends on the bitness of the system: for 32-bit types, their size will be 4 bytes, and for 64-bit ones, respectively, 8 bytes.

Integer constants are written as a sequence of decimal digits. Type of integer constants by default is int. It can be refined by adding suffixes. The suffix L (or l) attached to any constant forces the constant to be represented as a long. Similarly, the suffix U (or u) forces the constant to be uint. Integer constants can be decimal (base 10), octal (base 8) or hexadecimal (base 16). Decimal constants cannot use an initial zero. An integer constant that has an initial zero is interpreted as an octal constant. All constants starting with 0x (or 0X) are taken to be hexadecimal. Letters a, b, c, d, e and f (capital or small) are used for presentation of numbers over 9. For example:

int octal = 023; // 19
int hex = 0xEF;  // 239    

Starting from C# 7.0, you can use binary constants (using prefixes 0B or 0b) and separators for large numbers (using underscore character):

int binary = 0b101011; // 43
long large = 12_345_678_900;

Underscore characters can be also placed into hexadecimal and binary constants. Starting with C# 7.2, underscore character can be also placed between 0x (or 0b) and number itself:

int binary = 0b_10_1011;

A literal character value is any single Unicode character between single quote marks. You can use whether symbols of the current character set, or integer constant, which precedes the backslash character. It is a set of special control characters (these double characters are called escape sequences):

'\n' - a new line,
'\t' - horizontal tab,
'\r' - jump at the beginning of line, 
'\'' - single quote,
'\"' - double quote,
'\\' - backslash character itself.

Constants of real types can be written whether with decimal point or in scientific notation and are double by default. If necessary, type of constant can be specified by adding suffix f or F for type float, d or D for type double. Constants of decimal type use suffix M (m). For example:

1.5f    // 1.5  (float)
2.4E-2d // 0.25 (double)
12.5m   // 12.5 (decimal)

C# supports implicit type conversion. For arithmetic types, only "widening conversion" is allowed. You can convert integer values into their floating point representation.

Numbers without a decimal point are interpreted as integers (of int type). Constant expression of type int can be converted to the value of any integer (even narrower) if its value falls in the range for that type. The char data type can be implicitly converted to integers and floating types, but not vice versa. No implicit converting of float or double to decimal supported. You cannot assign floating point values to integer variables.

int   i  = 10;
float f  = i;        // Permissible conversion
long  l  = f;        // Error! Narrowing conversion

A narrowing conversion (converting from a larger type, for instance, double, to a smaller type, for instance, float) is dangerous because of potential lost of data.

C# supports so called cast, or explicit conversion:

(type) expression

A narrowing conversion must be explicit:

double d = 1;
long  k = (long) d; // Type cast

Numeric literals with decimal point are constants of type double. To assign them to smaller types, you must use explicit type cast:

float f  = 10.5;         // Error! Narrowing conversion
float f1 = (float) 10.5; // Type cast
float f2 = 10.5f;        // Clarification of the type constants. No errors

There is no conversion between the bool type and other types.

String literal consists of characters enclosed in double quotes. For example:

"A string"

The result of adding a string to a variable of another type converts the value to a string representation. In particular, this approach is used to display values of several variables. For example:

int k = 1;
double d = 2.5;
Console.WriteLine(k + " " + d); // 1 2.5

All reference types can obtain a null value (does not refer to any object). In addition, there is a special group of value types: so called nullable types. Variables of nullable types can receive values specified for particular type, plus the value null. To describe nullable types, construction type? is used, where the type is one of the possible value types, such as int?, double?, and so on. The value of nullable types cannot be implicitly converted into the appropriate value type, because you can lose possible null value. Therefore, you need an explicit conversion.

The use of byte, sbyte, short, and ushort (instead of int) is not recommended.

C# supports explicit pointers. Using pointers is not recommended.

Local variables can be defined and initialized in the same way as ones in C++. Unlike C++, you cannot declare variables without definition. You must initialize variable before use of its value. Otherwise, compile error is produced:

int m;
int n = m;    // Error!

C# 3.0 (and later versions) allows you to create implicitly typed local variables. To define such variable, you should use context-sensitive keyword var. These variables must be initialized. The variable type determined by compiler according to type of initialization expression. For example:

var i = 1;
var s = "Hello";

Despite the fact that the type is not specified explicitly, the compiler creates a variable of particular type. Once a variable is created, you cannot change its type:

var k = 1;
k = "Hello"; // Error!

Starting with C# 7.0, you can create references to value-typed variables. The ref modifier is used for this purpose:

double x = 1;
ref double z = ref x; // reference to x
z = 10;
Console.WriteLine(x); // 10

Starting with C# 7.3, references may be reassigned to refer to different variables after being initialized:

double x = 1;
ref double z = ref x; // reference to x
double y = 2;
z = ref y;            // reference to y

2.4.5 Expressions and Operations

C# supports almost all of the standard numeric, logical and comparison C++ operators. These standard operators have the same precedence and associativity in C# as they do in C++.

C# supports the comma operator only in loop headings. For example:

int i, j;
for (i = 0, j = 0; i < 10; i++, j += 2)
{
    Console.WriteLine(i + " " + j);
}

Starting with C# 7.2, you can use conditional operator to get a reference to a value-typed variable. For example:

int m, n;
// ...
ref int k = ref (m < n ? ref m : ref n); // refers to a variable with a smaller value

So called null-coalescing operator (??) is used to define a default value for nullable value types or reference types. This operator checks whether some variable (expression) is null or not and returns default value instead of null. For example,

a = b ?? c;    

is the same as:

a = b != null ? b : c;

C# 8.0 has introduced a new operator, a so-called null-coalescing assignment operator (??=). The value of its right-hand operand is assigned to the left-hand operand, only if the value of the left-hand operand is null. For example,

int? m = null;
m ??= 2; // m evaluated to 2
Console.WriteLine(m); // 2
m ??= 3; // m does not evaluated to 3
Console.WriteLine(m); // 2

The following table shows relative precedence of most used operators (in order from high to low).

Meaning Operator

brackets
access to an element
null conditional member access
index
postfix increment and decrement
creation of objects
size
turning overflow checking

(x)
x.y
x?.y

a[i]
x++, x--
new
sizeof
checked/unchecked
unary + and -
logical NOT
prefix increment and decrement
type cast
- +
!
++x --x
(T)x
multiplication, division, remainder from division * / %
binary addition and subtraction +, -
shift << >>
conditions < > <= >= is
equality == !=
bitwise AND &
bitwise XOR ^
bitwise OR |
logical AND &&
logical OR ||
conditional operators ?: ??
assignment
lambda operator
=, op=
=>

C# provides sizeof operator. This operator returns the size in bytes of the unmanaged types.

C# allows you to turn on and off overflow checking. checked and unchecked operators, followed by expression or block, are used for this purpose. The checked block produces System.OverflowException. unchecked block does not produce overflow exception. For example:

byte i = 255;
byte k = unchecked ((byte) (i + 1)); // k == 0

or

byte i = 255;
byte k; checked { k = ((byte) (i + 1)); // Exception! }

Bitwise operators can be applied to integer and Boolean operands.

2.4.6 Statements. Control over Program Execution

A statement is the smallest autonomous part of programming code. C# program is a sequence of statements. Most of C# statements are similar to C++ statements.

Empty statement consists of a semicolon.

Expression statement is a complete expression that ends with a semicolon. For example:

k = i + j + 1;        // assignment
Console.WriteLine(k); // function invocation

Compound statement is a sequence of statements enclosed in braces. Compound statement is often referred to as block. Compound statement does not contain semicolon after closing brace. Syntactically block can be considered as a separate statement, but it also defines scope of visibility. Identifier declared inside the block, has a scope from the point of definition to the closing brace. Blocks can be nested in each other.

Selection statement is whether conventional statement or switch statement. Conditional statement is used in two forms:

if (condition_expression)
    statement1
else
    statement2

or

if (condition_expression)
    statement1

If condition_expression is true then statement1 is executed, otherwise flow of control jumps to statement2 (in first form), or to the next statement (in second form). Unlike C++, condition can only be of type bool.

The switch statement allows you to select one of several possible branches of computing and based on the following scheme:

switch (expression)
    block

The block has the following form:

{
    case constant_1: instructions; break;
    case constant_2: instructions; break;
    ...
    default: instructions; break;
}

There are some differences in usage of switch statement in C# and С++. In C#, you must place whether break or goto at the end of each branch. For example,

switch (i) 
{
    case 1: Console.WriteLine("1");
            break;        // leaving switch
    case 2: Console.WriteLine("2");
            goto case 3;  // jump to another case
    case 3: Console.WriteLine("2 or 3");
          // Compile error: no goto statement!
    default:Console.WriteLine("other values");
            break;
}

The case statement without expression does require neither break nor goto statements.

Expression inside of switch header can return integer of string-type result. In the second case, you can test string constants:

case "some text":

The switch expression introduced in C# 8.0 allows you to simplify code:

type result = expression switch
{
    value1 => result1,
    value2 => result2,
    ...
    _      => default_result, // underscore interpreted as a default branch
};

Now we can give a small example. Assume variable b was created and calculated in some way:

bool? b = ... // can be false, true, or null

Now we want calculate k according to the following table:

b k
false
0
true
1
null
-1

The conventional approach requires the following code:

int k;
switch (b)
{
    case false:
        k = 0;
        break;
    case true:
        k = 1;
        break;
    case null:
        k = -1;
        break;
}

Now we can implement it easier:

var k = b switch
{
    false => 0,
    true  => 1,
    null  => -1
};

Looping constructs in C# are implemented the same way as analogous constructs in C++.

You can use goto statement and labels to control program execution. The only case where the use of goto is appropriate is interruption of several nested loops. For example:

int a;
  . . .
double b = 0;  
for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i + j + a == 0)
        {
            goto label;
        }
        b += 1 / (i + j + a);
    }
}
label:
// other statements

Additional foreach loop is used for traversal of arrays and collections.

2.5 Namespaces

Namespaces provide means for logical grouping of classes and other namespaces. The syntax and usage of namespaces is very close to one in C++. Here is an example of namespace definition:

namespace NewSpace
{
    public class SomeClass
    {
        public void f()
        {
        }
    }
    
    public struct SomeStruct
    {
    
    }
}

Namespace can be extended to other files. If you don't declare any namespace, default (global) namespace assumed.

As can be seen from the example, the namespace groups classes (and other types). Namespaces can be invested in each other. Instead of creating multiple nested spaces, you can describe namespaces with dots. For example, instead of such a description

namespace First
{
    namespace Second
    {
        namespace Third
        {
        }
    }
}    

can be applied more compact:

namespace First.Second.Third
{

}    

Version C # 10 allows you to determine the namespace to the end of the file without unnecessary braces (file-scoped namespace declaration). If you specify the name of the namespace with semicolon at the beginning of the file, this means that all the definitions to the end of the file refer to this namespace:

namespace SomeSpace;
//
// The entire code is defined in the SomeSpace namespace
//

To access particular elements, dot character is used. If you want to use several (or all of) the members of a namespace, C# provides an easy way to get access to the complete namespace. The using directive specifies that all identifiers in a namespace are in scope at the point that the using-directive statement is made. For example:

using NewSpace;

namespace OtherSpace { 
    public class Test {
        public void g() {
            SomeClass sc = new SomeClass();  // type name without prefix
            sc.f();
        }
    }
}

You can use an alternate name (alias) to refer to a namespace identifier:

using S = System;
...
    S.Console.WriteLine();

As seen from this example, the elements of a namespace can be accessed from alias using dot character. But there is a more appropriate use of :: operator. In this case, it is clear that we work with alias:

using S = System;
...
    S::Console.WriteLine();

C# provides a context-sensitive keyword global (alias to global namespace). For example:

global::System.Console.WriteLine();

This alias is used to prevent name conflicts.

In the version 10 of C#, the capabilities of using directive are extended. In particular, you can add a global modifier to any using directive. This directive will apply to all source files within the project.

2.6 Classes

2.6.1 Class Definition

All C# code can be placed only inside of classes. Here is an example of a simple class:

class Rectangle 
{
    public double Width;
    public double Height;
    public double Area()
    {
        return Width * Height;
    }
}

Unlike C++, non-abstract and non-partial methods are always implemented inside of a class (a structure) body.

When you create an object, fields are initialized with default values. You can also initialize fields explicitly:

class Rectangle
{
    public double Width = 10;
    public double Height = 20;
    public double Area()
    {
        return Width * Height;
    }
}

Class itself can be defined in namespace or inside of another class.

Class members can be defined with static keyword. You can access these elements without creation of object. C# does not allow you to access these elements using instance names. Only class names are allowed. You can initialize static fields inside of a class body:

class NewClass 
{
    public static double x = 10;
    public static int    i = 20;
}

You can create constants inside of a class body (a field with const modifier). Such constants are static by default. These constants are created by the compiler and cannot be changed anywhere else.

Class elements can be private, protected, public, internal, protected internal, and private protected. Internal access levels determine visibility within assembly. The private protected (introduced in C# 7.2) level allows access for derived classes declared in the same assembly. Class itself can be declared as public, otherwise it will be available only within the assembly. Unlike C++, C# specification requires a separate access declaration for each class member (or groups of data member described after a single type specifier). Elements without visibility specifier are private by default:

public class TestClass
{
    private int i;
    double x; // private 

    public void SetI(int value)
    {
        i = value;
    }
   
    public int GetI() 
    {
        return i;
    }
  
    public void SetX(double value)
    {
        x = value;
    }

    public double GetX()
    {
        return x;
    }
}

You can create an instance by applying new operator to constructor:

TestClass tc = new TestClass();

A new operator created an object in free store (heap). In previous example, tc is a name of a reference to an object. The value of reference is null if no objects are referenced.

A constructor is a function whose name matches with the class name and does not define any return type. Constructor is used for initialization of object. A class may define several constructors. If no constructor is defined by the class, a default constructor (without arguments) is automatically created by constructor. Default constructor initializes all the fields to their default values.

The this keyword refers to the current instance for which a method is called. The use of this keyword is very similar to appropriate pointer in C++. Static member functions do not obtain this reference.

You can invoke constructor with arguments from another constructor using this keyword. Such invocation is placed into constructor's initialization list between constructor's header and constructor's body.

class Rectangle
{
    double width;
    double height;
    public Rectangle(double width, double height) 
    {
        this.width = width;
        this.height = height;
    }
    public Rectangle() : this(10, 20) // invocation of another constructor
    {
    }
}

You can create so called static constructors for initalization of static. The definition of a static constructor is started with static keyword. You cannot define visibility for static constructors. You cannot access non-static class elements from static constructor. Static constructors are always invoked implicitly before any use of a class.

class Counter 
{
    static int count; 
    static Counter() // static constructor
    { 
        count = 0; 
    }
}

You can create both static and non-static constructors with the same argument lists within a single class.

The readonly modifier allows you to create non-static constants. The value of such constant can be set only by definition or in constructors. Different instances of a class can store different values of readonly fields.

class NewClass 
{
    readonly double x = 10;
    public NewClass(double initX)
    {
        x = initX;
    }
}

C# allows you to create destructors.

class NewClass 
{
    ~NewClass() // destructor
    {

    }
}

Destructor is always invoked by garbage collector by destruction of object. Sometimes garbage collector does not remove objects. Therefore, destructor will not be invoked. For some objects, you can create special method called Dispose(), which contains the finalization code, including the release of resources. To guarantee invocation of this method, special using construct can be applied:

using (X x = new X()) 
{
  
}
// call of Dispose()

Note: X class must implement the IDisposable interface. Interfaces will be discussed in the next topic.

The using keyword in C# allows you to create synonyms for class names:

using AliasToClass = NewSpace.SomeClass;
...
    AliasToClass atc = new AliasToClass(); // inside a function of some class

2.6.2 Definition and Use of Methods

Method is a function that is defined within the class and has direct access to other class members. Unlike C++, methods are always implemented within class body. C# does not support global functions. Instead of them, static class methods are used. Definition of a static function in the simplest case is as follows:

static result_type function_name(list_of_formal_parameters) body

Parameters (arguments) of function defined in brackets are called formal parameters. Parameters, which are used by invocation of a function, are called actual parameters. Storage for formal parameters is allocated by invocation of a function. Appropriate cells are created in the program stack. Values of actual parameters are copied into these cells.

Function's body is a block (compound statement). In the following example, function returns sum of two arguments:

static int Sum(int a, int b)
{
    int c = a + b;
    return c;
} 

We can omit c variable:

static int Sum(int a, int b)
{
    return a + b;
} 

To invoke function from another block, you should use its name followed by actual arguments (constants, variables, or expressions of appropriate types):

int x = 4;
int y = 5;
int z = Sum(a, b);
int t = Sum(1, 3); 

To invoke public static method of another class, you should use the following syntax:

Class_name.Method_name(actual_parameters)

In such form we invoke standard mathematical routines. For example

y = Math.Sin(x);

Starting from C# 6.0, you can import static members from particular class, for example:

using static System.Math;

Now you can invoke static methods defined in the Math class without qualifying those methods as members of that class. You must use the fully qualified class name in the using directive. Now you can use the following syntax:

y = Sin(x);

Some methods do not need parameters:

static int Zero()
{
    return 0;
}

The call of such function also requires empty brackets:

Console.WriteLine(Zero());

The return statement within function body causes termination of function's execution. The expression after return keyword defines a value, which will be return from this function.

Sometimes, function returns no value. To declare such function, void keyword is used as resulting type.

static void Hello()
{
    Console.WriteLine("Hello!");
}

In this case, you can omit return keyword in function's body. The return statement (without succeeding expression) allows termination of function before end of body. Function with void type can be called using separate statement only.

Hello();

Function also can be called from the body of the same function. Recursion is a mechanism of calling function from itself directly or indirectly.

By default, arguments of value types are transferred by value. Reference typed arguments are transferred by reference. Sometimes you need to transfer value-typed arguments by reference. There are two ways of definition such arguments. If you want to change old values of variables, you can define arguments with ref keyword.

public static void Swap(ref int a, ref int b) 
{
    int c = a;
    a = b;
    b = c;
}

At the point of invocation, you must prefix names of actual parameters with ref keyword. Variables those are used as actual arguments must be initialized first. Otherwise, compiler produces an error.

using System;

namespace SwapperApp
{
    class Swapper
    {
        public static void Swap(ref int a, ref int b) 
        {
            int c = a;
            a = b;
            b = c;
        }
 
        static void Main(string[] args)
        {
            int x = 1;
            int y = 10;
            Swap(ref x, ref y); 
            Console.WriteLine("x = " + x + " y = " + y); 
        }
    }
}

As you can see, integer variables, as well as variables of other types, can be converted to string type by adding such arguments to string type constants and variables (using "plus" operator).

Starting with C# 7.0, you can also use the ref modifier before function's returning type. In this case, you also need to add the ref modifier at function invocation:

static ref int SecondName(ref int n) 
{
    return ref n;
}

static void Main(string[] args)
{
    int k = 3;
    ref int k2 = ref SecondName(ref k);
    k2 = 4;
    Console.WriteLine(k);
}

Another need in transferring arguments by reference is getting several results from some function. The out keyword is used to specify the transfer of parameters by the reference. The compiler checks whether such parameters are assigned values in the body of the function. You must also specify the word out for the actual parameters when calling the function.

Suppose we need to solve the following equation:

|x| – b = 0

Since we get two roots, it is advisable to use parameters with out attribute:

using System;

namespace EquationWithAbsValue
{
    class Program
    {
        static void SolveEquation(double b, out double x1, out double x2)
        {
            x1 = b;
            x2 = -b;
        }

        static void Main(string[] args)
        {
            double b, x1, x2;
            b = 3;
            SolveEquation(b, out x1, out x2);
            Console.WriteLine("x1 = " + x1 + " x2 = " + x2);
        }
    }
}

Version 7.0 of C# allows definition of necessary variables with out modifiers inside the method invocation within the list of actual arguments. After returning from an invoked method you can use them:

...

        static void Main(string[] args)
        {
            double b = 3;
            SolveEquation(b, out double x1, out double x2);
            Console.WriteLine("x1 = " + x1 + " x2 = " + x2);
        }
    }
}

Starting from C# 7.2 you can use in modifier by sending argument by reference. Placing in keyword before value-typed parameter allows declaration of a parameter that you transfer by reference, but its value cannot be modified; it treated as some constant within a function's body. For example:

static double Sum(in double x, in double y)
{
    return x + y;
}

static void Main(string[] args)
{
    double x = 1, y = 2;
    Console.WriteLine(Sum(in x, in y)); // the in modifier is obligatory
}

If you'll try to modify values of such parameters in a function body, you'll get a compiler error:

static double Sum(in double x, in double y)
{
    x = 4; // Error!
    // ...
}

The reason in usage the modifier is efficiency of sending large data structures without copying.

Note: the in modifier works like reference to constant in C++.

Version 4.0 of C# allows description of the function with so called default parameters. This approach allows you to call a function with different number of parameters. For example:

static double Sum(double a, double b = 0, double c = 0)
{
    return a + b + c;
}

This function can be called by sending either one, or two, or three parameters:

double x = 0.1;
double y = 0.2;
double z = 0.3;
Console.WriteLine(Sum(x));
Console.WriteLine(Sum(x, y));
Console.WriteLine(Sum(x, y, z));

If the default values satisfy our needs, corresponding actual parameters may be omitted. Default parameters should be the last in argument list, otherwise we get a syntax error message:

static void F(double x, int y = 0, int h) { } // Syntax error!

Starting with C# 4.0, you can specify names of the formal parameters by invocation of functions. The following fragment program calculated the expression y = ax + b:

static double Y(double a, double x, double b)
{
    return a * x + b;
}

static void Main(string[] args)
{
    Console.WriteLine(Y(a: 2, x: 3, b: 4)); // 10
}    

This feature is especially useful in combination with default parameters. It allows you to specify only the required parameters. For example:

static double Y(double a = 1, double x = 0, double b = 0)
{
    return a * x + b;
}

static void Main(string[] args)
{
    Console.WriteLine(Y(a: 2, x: 3, b: 4)); // 10
    Console.WriteLine(Y(x: 5, b:11));       // 16
    Console.WriteLine(Y(x: 5));             // 5
    Console.WriteLine(Y());                 // 0
}

Starting from C# 7.0, you can create local functions. Now methods can be created inside the context of another method. The local method can be only called from the context in which is it declared. In C# 8.0 local functions can also be static. Such functions cannot have access to local variables and parameters. In the following example, the sum() local function cannot be static because it uses value of n (local argument). The cube() function is static because it gets all necessary information from its arguments list:

static int SumOfCubes(int n)
{
    return sum();
    // local function:
    int sum()
    {
        int result = 0;
        for (int k = 1; k <= n; k++)
        {
            result += cube(k);
        }
        return result;
    }
    // static local function:
    static int cube(int k)
    {
        return k * k * k;
    }
}

As you can see, names of local functions can start with small letters.

All non-static methods have implicitly reference to the object for which they are used.

2.7 Properties

Properties are class elements, which look just like fields, but from the inside, properties contain code that should be executed by getting and setting values concerned with properties. In most cases, properties represent actual class fields. Therefore, properties can be used instead of C++'s setters and getters.

To define property, you should set property type, followed by property type (by convention, property name should start from capitalized letter), followed by programming block. Within this block, you should create two subblocks started with set and get keywords.

The set {} block contains code that should be executed by setting a new value to property. You can use value keyword which represents value assigned to property. The get {} block is executed if property is used for reading. This block must contain return statement.

You can create read-only properties (without set {} block) and write-only properties (without get {} block). In the following example, class defines properties called FirstPower and SecondPower:

using System;

namespace PropertyTest
{
    class Properties
    {
        private double firstPower; // field
        public double FirstPower   // property
        {
            set
            {
                firstPower = value;
            }
            get
            {
                return firstPower;
            }
        }

        public double SecondPower // read-only property
        {
            get
            {
                return firstPower * firstPower;
            }
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            Properties cwp = new Properties();
            cwp.FirstPower = 10;                // setting
            Console.WriteLine(cwp.SecondPower); // getting
        }
    }
}

Traditionally property names start with a capital letter. You can transfer properties to functions as actual arguments, but only by value (without ref or out keywords). C# supports creation of static properties.

Version C# 3.0 (2007) provides so-called automatic properties:

public int X { get; set; }

Each such property concerned with automatically created private field which is invisible and cannot be accessed neither from inside nor from outside of a class. The access can be made through the property only. When you create an object, fields related to automatic properties are assigned the default values for their types. Other values can be assigned in constructors.

public class Point
{
    public int X { get; set; } // X = 0
    public int Y { get; set; } // Y = 0
    public Point()
    {
        X = 1;
        Y = 2;
    }
}

The main advantage of automatic properties compared to public fields is the ability of separate definition of visibility for writing and reading. For example, the following property can be read from any part of code but modified only within this class:

public int Z { get; private set; }    

Note: a similar possibility also added for ordinary (non-automatic) properties:

private int z;
public int Z
{
    get { return z; }
    protected set { z = value; }
}

Starting from version 6.0 of C# language, you can create read-only automatic properties:

public int X { get; }

Such properties can be set only in the body of a constructor (like a field with readonly modifier). Trying to set such property in another method generates a compiling error.

The C# 6.0 language version also allows initialization of automatic properties, for example:

int Initial { get; set; } = 1;

2.8 Working with Arrays

2.8.1 One-Dimensional Arrays

C# arrays are reference types.

The same syntax is used:

int[] a = new int[10];   // array of 10 integers
double[] b;
b = new double[20];
  

Array length is not a part of array type. You can assign a new array to the same reference (previous array elements will be lost):

a = new int[30];

The first array of 10 elements will be deleted by the garbage collector.

To determine the size of the array, you can use any integer expression. When you create an array, its elements are initialized by default values.

Unlike C++, the C# arrays are stored together with the number of elements. Number of array elements can always be accessed using special read-only property Length:

int[] a = new int[10];
Console.WriteLine(a.Length); // 10

You can access particular elements using indexing. Indexing is always starting from zero. The typical array traversal is as follows:

for (int i = 0; i < a.Length; i++)
{
    a[i] = 0;
}

You can fill array with initial values:

int[] a1 = new int[3] { 1, 2, 3 };
// You can omit size:
int[] a2 = new int[] { 1, 2, 3 };
// You can omit new:
int[] a3 = { 1, 2, 3 };

The following example demonstrates reading the number and values of elements from the keyboard.

using System;

namespace ArrayTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Enter a number of array elements:");
            int size = int.Parse(Console.ReadLine() ?? "0");
            double[] a = new double[size];
            Console.WriteLine("Enter array elements:");
            for (int i = 0; i < a.Length; i++)
            {
                a[i] = double.Parse(Console.ReadLine() ?? "0");
            }
            // Work with array
            // ...
        }
    }

}

For locally defined arrays you can implicitly specify type of elements. For example:

var a = new int[10];           // An array of int
var b = new[] { 1.5, 2, 4 };   // An array of double

Implicit initialization does not allow creation arrays with elements of different types. For example, the following definition produces a syntax error:

var c = new[] { 1, 'a', false }; // Error: elements of different types

Access to array elements carried out by indexing operation. If you attempt to exceed range of index, CLR throws an exception System.IndexOutOfRangeException:

double[] b = new double[10];
b[100] = 10;   // IndexOutOfRangeException

Array size can be obtained using Length property:

for (int i = 0; i < a.Length; a++) 
{
    a[i] = i;
}

foreach loop simplifies traversal of array elements:

foreach (type variable in array)
    cycle body

For example:

int[] a = {1, 2, 3};
foreach (int x in a)
{
    Console.WriteLine(x); // x is a current array item
}

foreach can be used for reading data only.

Array variables are reference types. Therefore, you cannot copy one array into another using assignment operator:

b = a; // now b refers to the same array

The System.Array class is the base class for all arrays. This class provides methods for creating, processing, searching and sorting arrays. For example, to copy an array, you can use static Copy() function of System.Array class. This function is implemented in two ways:

// Copy a specified number of elements from the beginning:
public static void Copy(Array from, Array to, int length);

// Copy a specified number of elements from fromIndex into a new array, starting from toIndex position:
public static void Copy(Array from, int fromIndex, Array to, int toIndex, int length);

Below is an example of both variants.

int[] a = { 1, 2, 3, 4};
int[] b = new int[4];
Array.Copy(a, b, a.Length); // b contains {1, 2, 3, 4}
// the same as Array.Copy(a, 0, b, 0, a.Length); 
a = new int[] { 10, 20, 30, 40};
// copying range
Array.Copy(a, 1, b, 2, 2);  // b contains {1, 2, 20, 30}    

Static Array.Resize() function allows you to change the size of an existing array. For example:

int[] a = { 1, 2, 3, 4 };
Array.Resize(ref a, a.Length + 1);
foreach (int x in a)
{
    Console.Write(x + " "); // 1 2 3 4 0
}

Other useful methods of System.Array will be considered later.

The C# 8.0 version introduces new types System.Index and System.Range. These types simplify work with array indices. In the simplest case we can use variables of Index type instead of integer indices:

int[] arr = { 20, 30, 40, 50 };
Index index = 0;
Console.WriteLine(arr[index]); // 20

The advantage of Index type is in possibility of applying ^ operator. In our case, ^1 means arr.length - 1, ^2 is arr.length - 2, etc.

index = ^1;
Console.WriteLine(arr[index]); // 50

Note: arithmetic operators, as well as comparison operators are not allowed for data of Index type.

Typically, the anonymous constants of the Index type are used:

Console.WriteLine(arr[^2] + " " + arr[^1]); // 40 50

The System.Range type represents a subrange of a sequence (an array). Typically, variables of System.Range type are initialized using .. operator:

Range range = 0..3; // indices 0, 1, and 2

A range specifies the start and end of a range, including the start and not including the end of the range. The Index type is used to determine the start and end of a range. For example, [0..^0] represents the entire range.

The usage of ranges provides the simplest way of getting subsequence from a source array. In the following example, we create an array of copies of items:

int[] arr = { 20, 30, 40, 50 }; // source array
int[] slice = arr[1..^0]; // 30 40 50

You can also create an anonymous slice to work with some subsequence of a source array:

Range range = 0..3; // indices 0, 1, and 2
foreach (var item in arr[range])
{
    Console.Write(item + " "); // 20 30 40
}

2.8.2 Multidimensional Arrays

There are two kinds of multidimensional arrays in C#:

  • ordinary multidimensional arrays
  • jagged arrays

There are examples of creation multidimensional arrays:

int[,]  c2 = new int [2, 3];      // two-dimensional array
int[,,] c3 = new int [3, 4, 5];   // three-dimensional array

You can initialize multidimensional array:

int[,] d = new int[2, 3]{{1, 10, 100}, {12, 13, 14}};

You can access array elements in such form:

d[1, 2] = 20;

The GetLength() method of System.Array class returns elements count for a given dimension. Here is a typical example of traversal of two-dimensional array:

int[,] arr = {{11, 12}, {21, 22}, {31, 32}};
for (int i = 0; i < arr.GetLength(0); i++)
{
    for (int j = 0; j < arr.GetLength(1); j++)
    {
        Console.Write(arr[i, j] + " ");
    }
    Console.WriteLine();
}

So called jagged arrays are in fact arrays of arrays. Each dimension of this array is a set of arrays as well. Different lines can have different sizes. Jagged arrays are similar to C++ multidimensional arrays ().

Creating a jagged array is done in two stages:

  • an array of references to arrays is created;
  • arrays with the required number of elements are created; references to them are written in the previously created array.

In the following example, we calculate sum of elements of jagged array:

using System;

namespace ArraysTest
{
    class ArrayClass
    {
        static void Main(string[] args)
        {
            int[][] arr = new int[3][];
            arr[0] = new int[] {1};
            arr[1] = new int[] {2, 3};
            arr[2] = new int[] {4, 6, 5};
            int[] sums = {0, 0, 0};
            for (int i = 0; i < arr.Length; i++)
            {
                for (int j = 0; j < arr[i].Length; j++)
                {
                    sums[i] += arr[i][j]; 
                }
            }
            for (int i = 0; i < sums.Length; i++)
            {
                Console.WriteLine(sums[i]);
            }
        }
    }
}

Initialization of jagged arrays can be done as follows:

double[][] a = { new double[] { 1 }, new double[] { 2, 3 } };    

2.8.3 Arrays as Parameters

Because arrays are reference types, they are always passed to functions as reference parameters:

static void Spoil(int[] a)
{
    a[2] = -100;
}

static void Main(string[] args)
{
    int[] a = { 1, 2, 3 };
    Spoil(a);
    foreach (int elem in a)
    {
        Console.Write(elem + " "); // 1 2 -100
    }
}    

To create functions with arbitrary number of arguments, params modifier followed by array-type argument is used. Function can have the only argument with params modifier. Such argument can be the last in argument list. Within function's body, you can handle with such argument as array type variable. There are two ways of transferring actual arguments:

  • transferring single array
  • transferring several elements which are interpreted as array elements

In the following example, function returns a sum of numbers:

    public static double sum(params double[] a)
    {
        double result = 0;
        foreach (double x in a)
        {
            result += x;
        }
        return result;
    }

    static void Main(string[] args)
    {
        Console.WriteLine(sum(1, 2.5));            // 3.5
        Console.WriteLine(sum(1, 2, 3, 4));        // 10
        double[] b = new double[5] { 1, 1, 1, 1, 1 };
        Console.WriteLine(sum(b));                 // 5
    }

2.9 Indexers

A special type of properties, so called indexer, allows applying index operator ([]) to object name. To create an indexer, you should define resulting type, followed by this keyword, followed by definition of index in square brackets. The rest of definition is the same as one in property definition. In the class below, the indexer provides access to a private field:

class HiddenArray
{
    private int[] arr = { 1, 2, 3 };
    public int this[int index]
    {
        get
        {
            return arr[index];
        }
        set
        {
            arr[index] = value;
        }
    }
}

Thanks to the indexer it is possible to work with object of the created class as with an array:

HiddenArray hiddenArray = new();
hiddenArray[0] = 4;
int k = hiddenArray[1];

The following class represents 3D point and provides indexer for handle with particular dimensions (1, 2, or 3):

using System;

namespace Point3DApp
{
    class Point3D
    {
        double x, y, z;
  
        public Point3D(double x, double y, double z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
  
        public double this [int index] // indexer
        {
            set
            {
                switch (index)
                {
                    case 1: x = value; break;
                    case 2: y = value; break;
                    case 3: z = value; break;
                }
            }
            get
            {
                switch (index)
                {
                    case 1:  return x;
                    case 2:  return y;
                    case 3:  return z;
                    // otherwise return maximal double value
                    default: return Double.MaxValue;
                }
            }
        }
    }
	
    class Test
    {
		
        static void Main(string[] args)
        {
            Point3D p3d = new Point3D(2, 3, 4);
            p3d[3] = 5;                   // writing
            for (int i = 1; i <= 4; i++)
            {
                Console.WriteLine(p3d[i]);// reading
            }
        }

    }
}

You can use non-integer index types.

2.10 Strings

2.10.1 Overview

Strings in C# are in fact instances of System.String class. Objects of this class contain Unicode characters. The string keyword is used as a synonym for class System.String. String is reference type.

You can create a String object simply by enclosing characters in double quotes:

string s = "First string";

String constant may be preceded by the @ (et) character. It is so called verbatim string. Processing these strings involves ignoring escape sequences such as \t, \n, \\, \" and so on. Such constants are useful for determining the path of the file, for example:

string path = @"c:\Users\Default";

This is the same as:

string path = "c:\\Users\\Default";

Individual characters can be accessed using square brackets. You can get the number of characters using Length property.

Some of the more important System.String methods are as follows:

Method Arguments Returns Description
CompareTo (string value) int Compares a string to the argument string. The result is a negative integer if this string object lexicographically precedes the argument string. The result is a positive integer if this string object lexicographically follows the argument string. The result is zero if the strings are equal
Equals (string value) bool Compares this string to the specified string. Returns true if strings are the same
IndexOf (string substring) int Returns the index location of the first occurrence of the specified substring. Returns -1 if character missing
IndexOf (char ch) int Returns the index location of the first occurrence of the specified character. Returns -1 if character missing
Substring (int beginindex, int endindex)> string Returns a new string that is a substring of the string
ToLower () string Returns the string in lowercase
ToUpper () string Returns the string in uppercase

The following example demonstrates the use of methods for processing of string data.

string s1 = "Hello World.";
int i = s1.Length;   // i = 12
char c = s1[6]; // c = 'W'
i = s1.IndexOf('e');   // i = 1 (index of  'e' in "Hello World.")
string s2 = "abcdef".Substring(2, 5); // s2 = "cde"
int k = "AA".CompareTo("AB");         // k = -1      

You can concatenate strings using + operator:

string s1 = "first";
string s2 = s1 + " and second";

If at least one of the operands is a string, we obtain string data representation:

int n = 1;
string sn = "n is " + n; // "n is 1"
double d = 1.1;
string sd = d + ""; // "1.1"

You can also use "+=" to append something to the end of the string.

You can create array of strings. The Sort() method of Array class allows you to sort arrays of strings. Strings are arranged alphabetically:

string[] a = { "dd", "ab", "aaa", "aa" };
Array.Sort(a); // aa aaa ab dd     

You can bypass all characters using foreach.

string s = "First";
for (int i = 0; i < s.Length; i++)
{
    Console.WriteLine(s[i]);
}
foreach (char c in s) // second way
{
    Console.WriteLine(c);
}

Note: starting from C# 8.0 strings, like arrays, support new features of indices and ranges.

The Split() method allows you to get an array of strings built from separate words of source string. For example:

string s = "The first sentence";
string[] arr = s.Split();
foreach (string word in arr)
{
    Console.WriteLine(word);
}

You can also get a new string using string.Format() method:

int k = 10;
double b = 2;
string s = string.Format("k = {0}, b = {1}", k, b);

The rules for formatting strings will be discussed later.

2.10.2 String Modification

An instance of System.String is said to be immutable because its value cannot be modified once it has been created. Methods that appear to modify a String actually return a new String containing the modification.

string s = "ab"; // one string in memory
s = s += "c";    // three strings in memory: "ab", "c", and "abc". s refers to "abc"
// Unnecessary strings will then be removed by garbage collector

There is a special class StringBuilder, allowing you to modify the contents of string object. This class is defined in the namespace System.Text. You create an object of type StringBuilder from the existing string. After modification, you can create a new object of class String, using the object of StringBuilder. For example:

string s = "abc";
StringBuilder sb1 = new StringBuilder(s);    // Constructor invocation
StringBuilder sb2 = new StringBuilder("cd"); // Constructor invocation
// modification of sb1 and sb2
// ...
string s1 = sb1 + "";        // Type conversion
string s2 = sb2 + "";        // Type conversion

The StringBuilder class provides several methods for modification of existing strings. These methods are Append(), Remove(), Insert(), Replace(), etc. Consider using these features in the following example:

using System;
using System.Text;

namespace StringBuilderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abc";
            StringBuilder sb = new StringBuilder(s);
            sb.Append("d");         // abcd
            sb[0] = 'f';            // fbcd
            sb.Remove(1, 2);        // fd
            sb.Insert(1, "gh");     // fghd
            sb.Replace("h", "mn");  // fgmnd
            Console.WriteLine(sb);
        }
    }
}

Using StringBuilder can increase the effectiveness of the program when a specific string undergoes multiple modifications within the program.

2.10.3 String Interpolation

An additional feature, which was added in C# 6, assumes possibility of embedding expressions into string. This possibility is called string interpolation. The first part of an appropriate of an expression is some format string prefixed by $ character. Within this format string you can allocate expressions whose resulting values will be converted into string representation and resulting string will be formed. For example:

string s = $"{7 - 5} * {1 + 1} = {1 + 3}";  // 2 * 2 = 4

Expressions allow formatting of results. To do this, place a colon after the expression and then specify formatting sequence, which starts with capital or small letters showing the format type:

d - decimal (integer) number
f - real number of fixed-point
e - exponential form of the number
x - hexadecimal number

There are also other formatting characters. After these characters you can add integer values that determine width of the output field. For example:

int i = 10;
double x = 2012;
string s = $"i = {i:d8} x = {x:f}, the same: {x:e}";
WriteLine(s); // i = 00000010 x = 2012.00, the same: 2.012000e+003

Formatting capabilities are similar to those used during console output.

2.11 Console Output and Input

The System.Console class is used for console output and input. The Write() function prints specified data from the current location within console window. In the simplest case function accepts a parameter of arbitrary type. The WriteLine() method appends a newline character to the output string. Invocation of WriteLine() without parameters conditions transition to the next line.

As the first parameter of Write() and WriteLine() methods you can specify the output format string. In the curly brackets indicate indexes parameters listed below. In fact, the values are inserted into output string at the specified locations. For example, after the following code snippet,

int k = 10;
double b = 2;
Console.WriteLine("k = {0}, b = {1}", k, b);    

we get the following output on the console window:

k = 10, b = 2

You can format your output. To do this, specify the formatting parameter after the parameter index. Formatting capabilities are similar to those used for string interpolation. For example:

int i = 10;
Console.WriteLine("{0:d8}", i);      // 00000010
Console.WriteLine("{0:x}", i);       // a
double d = 2012;
Console.WriteLine("{0:f6}", d);      // 2012.000000
Console.WriteLine("{0:f} {0:e}", d); // 2012.00 2.012000e+003    

The last example shows that a single value can be displayed several times using different formatting.

To enter data, ReadLine() method of Console class is used. This function returns a string that can be converted into the required number using the static Parse() method, implemented for standard value types (int, double, etc.). For example:

int i = int.Parse(Console.ReadLine() ?? "0");
double d = double.Parse(Console.ReadLine() ?? "0");

Use of ?? operator here is strongly recommended because ReadLine() method can potentialy return null.

The TryParse() method allows you to read value from some string (first parameter) and put converted value into given variable (second parameter). The boolean result can be either true (conversion successful) or false (conversion failed). For example:

double z;
if (double.TryParse(ReadLine(), out z))
{
    // Working with the value of z
}
else
{
    WriteLine("Wrong number");
}

3 Sample Programs

3.1 Working with Switch

Suppose you want to create a program that reads integer value of x from keyboard and calculates y according to the following table:

x
y
1
12
2
14
other values
16

The program (based on C#7) can be as follows:

using System;

namespace Switcher
{
    class Program
    {
    
        static void Main(string[] args)
        {
            int x = int.Parse(Console.ReadLine() ?? "0");
            int y;
            switch (x)
            {
                case 1:  y = 12; break;
                case 2:  y = 14; break;
                default: y = 16; break;
            }
            Console.WriteLine(y);
        }
    }
}

This program can be essentialy simplified using new features of C# 8 (switch expression) and C# 9 (implicit main class):

using static System.Console;

int x = int.Parse(ReadLine() ?? "0");
int y = x switch
{
    1 => 12,
    2 => 14,
    _ => 16,
};
WriteLine(y);

3.2 Working with Local Methods and ref Modifier

The following program shows the working of the local function that replaces values of two variables with their arithmetic means. It uses local function:

using static System.Console;

if (double.TryParse(ReadLine(), out double a) && double.TryParse(ReadLine(), out double b))
{
    replaceWithArithmeticMean();
    WriteLine("a = {0} b = {1}", a, b);
}
else
{
    WriteLine("Wrong number");
}

void replaceWithArithmeticMean()
{
    var c = (a + b) / 2;
    a = b = c;
}

The function is local because all code of this file is implicit body of the Main() function.

The disadvantage of this approach is the possibility to modify the only pair of variables. If we want to apply this function to several pairs of variables, we can create static local function with parameters:

using static System.Console;

if (double.TryParse(ReadLine(), out double a) && double.TryParse(ReadLine(), out double b))
{
    replaceWithArithmeticMean(ref a, ref b);
    WriteLine("a = {0} b = {1}", a, b);
}
else
{
    WriteLine("Wrong number");
}

double c = 3, d = 4;
replaceWithArithmeticMean(ref c, ref d);
WriteLine("c = {0} d = {1}", c, d);

static void replaceWithArithmeticMean(ref double a, ref double b)
{
    var c = (a + b) / 2;
    a = b = c;
}

This approach is more universal.

3.3 Use of Nullable Types and out Parameter Modifier

Suppose you want to develop a function that returns reciprocal value. This function cannot be calculated if the argument is 0. We can use nullable types.

using static System.Console;

namespace ReciprocalTest
{
    class Program
    {
        static double? Reciprocal(double x, out int errorCode)
        {
            errorCode = 0;
            if (x == 0)
            {
                errorCode = -1;
                return null;
            }
            return 1 / x;
        }

        static void Main(string[] args)
        {
            Console.Write("Enter x: ");
            double x = double.Parse(Console.ReadLine() ?? "0");
            double? y = Reciprocal(x, out int errorCode);
            WriteLine(errorCode == 0 ? y : "Error");
        }
    }
}

3.4 Linear Equation

Suppose we want to design a class that represents a linear equation. The fields of this class are coefficients a and b, and the root (x), which should be found. The following program can be created:

using System;

namespace LinearEquation
{
    public class LinearEquation
    {
        public double A  { get; set; }
        public double B  { get; set; }
        public double? X { get; private set; }

        public LinearEquation()
        {
            A = B = 0;
            X = null;
        }

        public int Solve()
        {
            if (A == 0)
            {
                if (B == 0)
                {
                    return -1; // infinite number of roots
                }
                else
                {
                    return 0;  // no roots
                }
            }
            X = -B / A;
            return 1;      // one root
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            LinearEquation e = new LinearEquation();
            e.A = double.Parse(Console.ReadLine() ?? "0");
            e.B = double.Parse(Console.ReadLine() ?? "0");
            switch (e.Solve())
            {
                case -1: Console.WriteLine("Infinite number of roots"); break;
                case 0:  Console.WriteLine("No roots"); break;
                case 1:  Console.WriteLine("X = " + e.X); break;
            }
        }
    }
}

3.5 Product of Numbers Entered from the Keyboard

In the following program, integers are read from the keyboard and appended to the array. Reading terminates with entering zero, which is not appended to the array. Then we calculate the product of the elements.

using System;

namespace Product
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] a = { };
            int k;
            do
            {
                k = int.Parse(Console.ReadLine() ?? "0");
                if (k == 0)
                {
                    break;
                }
                Array.Resize(ref a, a.Length + 1);
                a[a.Length - 1] = k;
            }
            while (true);
            int product = 1;
            foreach (int x in a)
            {
                product *= x;
            }
            Console.WriteLine(product);
        }
    }
}

3.6 Using Two-Dimensional and Jagged Arrays

Assume that we need to develop a program that is defined and initialized two-dimensional array of real numbers, and then creates jagged array which rows contain positive elements copied from the appropriate rows of the first array.

using System;

namespace JaggedArray
{
    class Program
    {
        static void Main(string[] args)
        {
            double[,] a = {{1.5, 0, -1},
                           {-12, -3, 0},
                           {7, 10, -11},
                           {1, 2, 3.5}};
            double[][] b = new double[a.GetLength(0)][];
            for (int i = 0; i < a.GetLength(0); i++)
            {
                int count = 0;
                for (int j = 0; j < a.GetLength(1); j++)
                {
                    if (a[i, j] > 0)
                    {
                        count++;
                    }
                }
                b[i] = new double[count];
            }
            for (int i = 0; i < a.GetLength(0); i++)
            {
                int k = 0;
                for (int j = 0; j < a.GetLength(1); j++)
                {
                    if (a[i, j] > 0)
                    {
                        b[i][k++] = a[i, j];
                    }
                }
            }
            for (int i = 0; i < b.Length; i++)
            {
                for (int j = 0; j < b[i].Length; j++)
                {
                    Console.Write(b[i][j] + " ");
                }
                Console.WriteLine();
            }
        }
    }
}

3.7 Sum of Digits

Sum of digits can be calculated using string representation of a given number:

using System;

namespace SumOfDigits
{
    class Program
    {
        static void Main(string[] args)
        {
            String n = Console.ReadLine() ?? "";
            int sum = 0;
            for (int i = 0; i < n.Length; i++)
            {
                sum += int.Parse(n[i] + "");
            }
            Console.WriteLine(sum);
        }
    }
}

3.8 Removing Unnecessary Spaces

The following program removes unnecessary spaces from the string (leave one).

using System;

namespace SpaceRemover
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = Console.ReadLine() ?? "";
            while (s.IndexOf("  ") >= 0)
            {
                s = s.Replace("  ", " ");
            }
            Console.WriteLine(s);
        }
    }
}

For example, if you type such text

To    be  or not to                be

you'll obtain the following result:

To be or not to be

3.9 Processing Data about Books on Bookshelf

Assume that we need to develop console application for processing data about books on the bookshelf. Data concerned with particular book consists of the title (string), year of publication (int) and the list of authors. To describe the author, we can first use the string type. It is necessary to implement a method of searching books, which titles contain given sequence of characters.

We need to create a new project first. It will be console application (New Project | Console App) named LabFirst. We also need to set a new name (Bookshelf) for our solution (Solution name). Now we get the following source code within file called Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LabFirst
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

All new classes will be added to LabFirst namespace. This can be done manually or by using the Insert Snippet... function of the context menu. To implement this, the following steps are needed:

  • add a blank line inside LabFirst namespace, before the definition of Program class
  • place the cursor on the new blank line
  • choose from the context menu Snippet | Insert Snippet... | Visual C# | class

A new empty class named MyClass appears on the specified place. This name should be changed to Book. New public properties, Title, Year, and Authors, should be added to our class. Unnecessary directives can be removed. The code will be as follows:

using System;

namespace LabFirst
{
    public class Book
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public string[] Authors { get; set; }
    } 

    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

After describing the properties we'll add constructor. Its parameters correspond listed properties. Unfortunately, Visual Studio does not allow generating constructors automatically. The manually added code would look like this:

public Book(string title, int year, params string[] authors)
{
    Title = title;
    Year = year;
    Authors = authors;
}    

The output data method is also useful. It will look like this:

public void Print()
{
    Console.WriteLine("Title: \"{0}\". Year of publication: {1}", Title, Year);
    Console.WriteLine("   Authors:"); 
    for(int i = 0; i < Authors.Length; i++)
    {
        Console.Write("      {0}", Authors[i]);
        if (i < Authors.Length - 1)
        {
            Console.WriteLine(",");
        }
        else
        {
            Console.WriteLine("");
        }
    }
}

The next class is Bookshelf. It contains an array-type property named Books (the array of books). We need to add the appropriate constructor:

public class Bookshelf
{
    public Book[] Books { get; set; }
    
    public Bookshelf(params Book[] books)
    {
        Books = books;
    }
}

The class should provide methods for displaying shelf contents and for finding the sequence of letters in the title:

public void Print()
{
    Console.WriteLine("----------Books:----------");
    foreach (Book book in Books)
    {
        book.Print();
    }
}

public Book[] ContainsCharacters(string characters)
{
    Book[] found = new Book[0];
    foreach (Book book in Books)
    {
        if (book.Title.Contains(characters)) 
        {
            // Adding a new element to the array:
            Array.Resize(ref found, found.Length + 1);
            found[found.Length - 1] = book;
        }
    }
    return found;
}    

Now we can test designed methods within Main() function of Program class:

static void Main(string[] args)
{
    // Create a new shelf with three books:
    Bookshelf bookshelf = new Bookshelf(
        new Book("The UML User Guide", 1999, "Grady Booch", "James Rumbaugh", "Ivar Jacobson"),
        new Book("Pro C# 2010 and the .NET 4 Platform", 2010, "Andrew Troelsen"),
        new Book("Thinking in Java", 2005, "Bruce Eckel")
    );
    // Display source data:
    bookshelf.Print();
    Console.WriteLine("Enter sequence of characters:");
    string sequence = Console.ReadLine() ?? "";
    // Looking for books with a certain sequence of characters:
    Bookshelf newBookshelf = new Bookshelf(bookshelf.ContainsCharacters(sequence));
    // Display result:
    newBookshelf.Print();
}

Here is the full text of Program.cs file:

using System;

namespace LabFirst
{
    public class Book
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public string[] Authors { get; set; }

        public Book(string title, int year, params string[] authors)
        {
            Title = title;
            Year = year;
            Authors = authors;
        }

        public void Print()
        {
            Console.WriteLine("Title: \"{0}\". Year of publication: {1}", Title, Year);
            Console.WriteLine("   Authors:"); 
            for(int i = 0; i < Authors.Length; i++)
            {
                Console.Write("      {0}", Authors[i]);
                if (i < Authors.Length - 1)
                {
                    Console.WriteLine(",");
                }
                else
                {
                    Console.WriteLine("");
                }
            }
        }
    }

    public class Bookshelf
    {
        public Book[] Books { get; set; }

        public Bookshelf(params Book[] books)
        {
            Books = books;
        }

        public void Print()
        {
            Console.WriteLine("----------Books:----------");
            foreach (Book book in Books)
            {
                book.Print();
            }
        }

        public Book[] ContainsCharacters(string characters)
        {
            Book[] found = new Book[0];
            foreach (Book book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Adding a new element to the array:
                    Array.Resize(ref found, found.Length + 1);
                    found[found.Length - 1] = book;
                }
            }
            return found;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Create a new shelf with three books:
            Bookshelf bookshelf = new Bookshelf(
                new Book("The UML User Guide", 1999, "Grady Booch", "James Rumbaugh", "Ivar Jacobson"),
                new Book("Pro C# 2010 and the .NET 4 Platform", 2010, "Andrew Troelsen"),
                new Book("Thinking in Java", 2005, "Bruce Eckel")
            );
            // Display source data:
            bookshelf.Print();
            Console.WriteLine("Enter sequence of characters:");
            string sequence = Console.ReadLine() ?? "";
            // Looking for books with a certain sequence of characters:
            Bookshelf newBookshelf = new Bookshelf(bookshelf.ContainsCharacters(sequence));
            // Display result:
            newBookshelf.Print();
        }
    }
}

4 Exercises

  1. Create classes with constructors and properties to describe the student and his (her) grades.
  2. Create classes with constructors and properties to describe the bookstore and books.
  3. Create a class with a constructor to describe a point in 3D space.
  4. Create a class with a constructor for description of some product (title and price are stored).
  5. Create a class with a constructor to describe the user (username and password).
  6. Create a function with two reference type parameters that increases one parameter by 1 and decreases the other by 2.
  7. Create a one-dimensional array with even count of items. Split this array into two halves. Use features of the System.Range class.
  8. Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with ones, and negative values with zeros.
  9. Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with average of all elements.
  10. Create a class for representation of a parallelepiped. Provide access to the length, width and height as by property names, and through the indexer.
  11. Enter sentence and display all its words in separate lines.
  12. Enter sentence, concatenate all its words and display result.

5 Quiz

  1. What is the difference between the .NET Framework and .NET Core?
  2. What is relationship between C# built-in types and standard CLR types?
  3. What are the benefits of using checked/unchecked blocks?
  4. What are advanced features of C# switch?
  5. How to create namespaces? How to access names from namespaces?
  6. What are main elements of class definition?
  7. What visibility levels of class elements supports C#?
  8. How to determine access level within assembly or within namespace?
  9. Is it always necessary to explicitly initialize class fields?
  10. What are the differences between static and non-static class members?
  11. What is encapsulation?
  12. What is the usage of this reference?
  13. How many constructors without arguments can be defined within a single class?
  14. How to invoke constructor from another constructor?
  15. Use of readonly modifier. How to initialize readonly members?
  16. Destructors in C#. When destructors are called?
  17. What are the benefits of the using block?
  18. What are differences between ref and out modifiers?
  19. Concept of properties. What are special features of properties?
  20. What is different in the description of properties and methods?
  21. How to create a write-only and read-only properties?
  22. What are advantages and disadvantages of automatic properties?
  23. Why use the Index type?
  24. What are the features of multidimensional arrays in comparison with C++?
  25. What is specific in creation and initialization of jagged arrays?
  26. When we use function arguments with params attribute?
  27. Description of indexers. What is different in use of objects with indexers and arrays?
  28. What is common and what are the differences in the use of strings in C# and C++?
  29. What are verbatim strings and where they should be applied?
  30. How to modify contents of previously created string?
  31. How to modify particular character within string object?
  32. What are advantages and disadvantages of StringBuilder class versus String class?
  33. How to format data output?

 

up