Laboratory Work 1

Fundamentals of OOP. Basic Syntax of the C# Programming Language

1 Laboratory Work Tasks

1.1 Individual Task

Create a C# console application that creates an array of strings according to one of the variants listed in the table. Search and sort the array according to the given criteria:

Data stored in an array of strings Search criteria Sorting criteria
Student's surname By the first letter of the surname By length
Name of an academic subject By a case-sensitive sequence of characters By the number of words in the name
Name of a city district By the last letter of the name By length
Name of the city Name length greater than a specified value Alphabetically
Surname of the participant of the sports section The length of the surname is less than the specified number By length
Surname of a football club player By the last letter of the surname By length in reverse order
Surname of a member of a musical band The length of the surname is greater than the specified value Alphabetically in reverse order
Title of a music album The number of words in the title is more than specified number Alphabetically
The name of the song The number of words in the name is equal to the specified number Alphabetically in reverse order
The name of a room in an apartment By the specified number of letters Alphabetically in reverse order
Title of a short story in a collection By the sequence of letters, taking into account the case By the number of words in the title
Title of an artist's work By the presence of a certain word By the number of words in the title in reverse order
Name of a metro station By a case-sensitive character sequence Alphabetically
Name of a railway station By the first letter of the name Alphabetically in reverse order
The title of the writer's novel The number of words in the title is less than specified By the number of words in the title in reverse order

Notes:

  • it is desirable that the task is chosen based on interests, and not based on the student's number;
  • the task cannot appear more than twice in the same academic group;
  • the list can be expanded upon agreement with the instructor.

Provide two implementations:

  • using traditional language constructs (branching, loops, etc.);
  • using methods of the Array class and lambda expressions.

Do not use

  • LINQ query expressions (from, select, where, orderby, etc.);
  • regular expressions.

The data to be searched will be entered from the keyboard at runtime. The test data must be prepared in such a way that the search gives more than one result.

1.2 Use of Nullable Types

Develop a program that defines a local function with a parameter of type double (argument x) which calculates the square root of its argument and returns double?. This function should return null if the square root of the argument cannot be computed. Test the function using 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 the difference between two consecutive approximations is less than the specified precision.

Note: do not use Math.Sqrt() and Math.Pow().

1.3 Greatest Common Divisor

Create a console application that contains a local function in its Main() method. This function calculates the greatest common divisor of two positive integer numbers using the simplest version of Euclid's algorithm. You should call the created local function for different arguments and demonstrate its operation.

1.4 Working with Jagged Arrays

Develop a program in which a regular two-dimensional integer array is declared and initialized, and a jagged array is created, whose rows contain the even items of the rows of the first array.

2 Guidelines

2.1 Fundamentals of OOP

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 goto statements. Modern programming methodology includes a large number of paradigms, the most important 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 involves creating separate blocks in the form of separate procedures with the ability to save the application state.
  • Modular programming involves the division of a program into independent logical and physical units that can be developed and processed separately.
  • 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 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 by cloning existing objects rather than defining classes (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 applications 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 Imperative Programming Methodology

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. Imperative programming, in which the necessary sequence of actions is determined, is often 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:

  • variable declarations;
  • 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 branching statements, loops, and sequences of statements. Due to the presence of loops with a precondition, postcondition, or parameter, the program can be fully implemented without conditional and unconditional jumps.

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);
  • loops: with precondition (while), with postcondition (do...while), with parameter (for);
  • code block – one or more statements enclosed in block braces (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 of the concept of a subroutine (procedure, function) that defines its own scope and may return a result, as well as means of calling subroutine with the subsequent use of this result. When a subroutine is called, 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 control flow from subroutines to the calling subroutine (the main program, in particular). In addition to return 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 methods defined in 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 industry faced challenges due to a significant increase in the complexity of software systems. The appearance of interactive 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 incompatible with the concept of data-driven programming.

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

  • the complexity of the domain;
  • the complexity of managing the development process;
  • the complexity 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 Ivan Sutherland's Sketchpad system and the description of the light pen.

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

The first universal object-oriented language was Smalltalk. Its widely used version was 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 designing the 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 has made it possible to manage software complexity by controlling the relationships between individual entities and enables the development of software suitable 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 ordinary (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. Some 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.

Three main concepts underlie object-oriented programming 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 client code. Data (fields) are accessed through public access functions or properties.

Inheritance is a mechanism for creating derived classes from base classes. Creating a derived class involves extending a base class 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 members of the base classes. In most object-oriented programming languages, only public inheritance is supported: members retain their visibility during inheritance. In this case, private members are inherited, but become unavailable for direct access in derived classes.

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

Runtime polymorphism allows an object's behavior to be determined at runtime. Classes that support runtime polymorphism, are called polymorphic classes.

Connecting the function body to the point of its invocation is called binding . If it occurs before the start of program execution, it is called early binding. This type of binding 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 binding mechanism is used to implement polymorphism.

In object-oriented programming languages, late binding is implemented through the mechanism of virtual methods. A virtual method (virtual function) is a method defined in a base class and overridden in derived classes, so that the specific implementation of the called method will be determined during program execution. The choice of implementation of the virtual method 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 methods 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.1.9 Basic Principles of OOP. Design Patterns

The effectiveness of the application of OOP is based not only on adequate real world modeling, but also on the application of fundamental principles, the observance of which improves the ability to manage the project, helps us to create higher-quality, more flexible, and reusable code. One of the most widely known sets of principles is the SOLID set of five principles:

S Single responsibility principle A class should have only one reason to change
O Open/closed principle Classes should be open for extension but closed for modification
L Liskov substitution principle Subtypes can replace their base types without changing the code
I Interface segregation principle There should be many specialized interfaces instead of one universal interface
D Dependency inversion principle Abstractions should not depend on the details, the details should depend on abstractions

The use of SOLID principles will be considered in detail later.

Software design pattern is a description of the interaction of objects and classes adapted to solve a particular problem in a particular context. Design patterns will also be considered in the context of creating more complex object-oriented solutions.

2.2 .NET Platform and C# Programming Language

2.2.1 General Concepts of the .NET Platform

.NET platform is a software environment that provides an environment for developing and running applications, based on the common language infrastructure: a common runtime environment and a common type system. The .NET platform, like the Java platform, allows you to create programs that can be executed on different operating systems without recompiling. The .NET architecture is based on a set of standards developed by the World Wide Web Consortium (W3C), primarily including the HTTP (which serves as a fundamental protocol for Web services) and the XML language.

Compared with the previous technologies offered by Microsoft, .NET has the following advantages:

  • managed code;
  • a unified standardized class library based on a hierarchy with a single root;
  • the ability to design self-describing components that do not require external registration for execution.

.NET Framework was an implementation of the .NET platform developed by Microsoft Corp.

In parallel with the development of new versions of the .NET Framework, Microsoft released .NET Core, a modular open-source platform for Windows, Linux and macOS. Several versions were released, up to version 3.1. 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 technologies they supported. In 2020, Microsoft unified the two branches in the .NET 5 platform.

Version 10 of .NET, released in 2025, provides long-term support (until 2028).

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

.NET includes two main components:

  • the 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, remote code execution and security. CLR enables application development using 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 consists of 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 (JIT) is used. Compiling on this stage takes place first time and each time when project parts are modified. On subsequent launches, the code executes immediately.

2.2.2 Common Type System

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

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

Reference types contain references to data stored in memory. Memory for such data is allocated on the so-called managed heap. There are three groups of reference types:

  • self-describing types, including class types and arrays;
  • pointer types;
  • interface types.

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

All CLR types form a common hierarchy with System.Object as their common base class.

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

2.2.3 Assemblies

Assembly is a set of types (with their implementation) and resources which are designed to work together and form a logical unit of application functionality.

An assembly contains code that is executed by the CLR. Assembly consists of one or more files. A special data block called the assembly manifest contains metadata. Metadata contains information about the resources, which are exported by assembly.

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

Assemblies can be 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 resource files. Dynamic assemblies are created in memory and executed.

A so-called Global Assembly Cache (GAC) is maintained on computers where CLR is installed. It contains different versions of assemblies used by different applications.

2.2.4 Key Features of the C# Programming Language

Although .NET supports the use of different programming languages, C# provides the most natural support for the .NET platform.

C# incorporates the best features of languages such as C++, Visual Basic, Java and Object Pascal. The key features of C# include:

  • support for object-oriented programming model;
  • built-in support of CTS types;
  • strict typing;
  • support for properties and events;
  • support for operator overloading and indexers;
  • automatic garbage collection;
  • the ability to manipulate pointers and access memory directly (in unmanaged code);
  • support for attributes.

The latest stable version of the language is C# 14.0 (supported by the latest versions of Visual Studio 2026).

Unlike C++, C# does not support global variables and functions. This is done to prevent name conflicts. The program consists of one or more type definitions (classes, interfaces, structures, enumerations, and delegates). Types are organized into namespaces, which logically group them.

Classes are the most common and versatile types. They contain fields, methods, properties, and other members. One of the classes that make up the application should define a static Main() method, which defines the program's entry 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 Creating a C# Console 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 choose which components you need to install. Components are grouped into so-called Workloads. In our case, it is enough to choose the following workload: .NET desktop development. Then you can press the Install button. After installation, you should register using a Microsoft account. This account can be created for free.

By default, after loading Visual Studio a start page is displayed. The Get Started panel contains a list of recommended ways to get started. You can create a new project directly. If you select the 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;
  • 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 the listed ways. Now a new popup window called Create a new project appears on the screen. You need to choose a project template. 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 a group of logically related projects that share common properties and settings. A project includes a set of source files and associated metadata such as references and build 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 Hello subdirectory of the Labs folder. The text is as follows:

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

The program contains the declaration 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 defines the program's entry point. Now program can be started by selecting Debug | Start Without Debugging function of the main menu. You also can press Ctrl+F5. As expected, the console window displays the expected text.

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

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

Sometimes you want to get an exit code that the program returns to the operating system. A value of 0 (zero) denotes a successful completion, any other integer value can indicate an error. The Main() method 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() method, as well as Program class, can be declared as public.

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

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

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

Visual Studio environment provides convenient debugging tools. 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 the program, use Stop Debugging function (Shift + F5).

2.4 Basic Syntax of C#

2.4.1 Preprocessor Directives

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

  • conditionally exclude sections of code (#define, #undef, #if, #elif, #else, #endif);
  • generating warnings and error messages (#warning and #error with corresponding error lines);
  • define named regions of code for outline display (#region and #endregion).

The last pair of directives allows creation of a 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, the mechanism of header files is not supported.

2.4.2 Comments

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

  • C-style comments (/* */);
  • C++-style comments (//);
  • XML comments used for generating documentation (///).

Generating documentation from comments allows you to obtain standardized documentation, which describes the elements of the source code. If you add specially designed comments to individual code elements, an XML document can be generated. This document, in turn, can be used to produce standard documentation files (Help).

Note: in order for a documentation file to be generated during compilation, in the Visual Studio project options in the Build | Options subtree you should select the Documentation file option; you can also specify the name of the file in which the documentation will be generated (XML documentation file path).

2.4.3 Identifiers, Keywords, and Reserved Words

The source code consists of tokens. A token is a sequence of characters that have a particular meaning as a whole. Tokens are separated by delimiters such as spaces, tabs, and newline characters. Tokens are divided into the following groups:

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

Like C++, 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 ("_"). Subsequent characters may also be digits.

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 are written without spaces, with the second, third, and subsequent words beginning with a capital letter. For example, you can create a variable name:

thisIsMyVariable

For meaningful names, it is advisable to use English mnemonics. Names of namespaces, types (classes, interfaces, enumerations, structures, and delegates), methods, public fields and properties are started with a capital letter. Other names begin with a lowercase letter.

Local variables are defined within methods. Variable declaration is similar to that in 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 identifiers in an inner block if they have already been defined in an 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.

Constants (literals) are used to initialize variables and in 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, when applied to a variable definition, means that its value cannot be changed 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. Types can be

  • value types: data is stored directly in the variable that was created (for example, in the call stack, if it is a local variable);
  • reference types: the variable does not directly contain data, but stores a reference to an object (object address), which is stored in dynamic memory.

Value types are structures and enumerations. Reference types are classes, interfaces, records, and delegates.

The C# types correspond to ones in the Common Type System (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 system architecture: on 32-bit systems 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 L or l (long) and U or u (uint). Integer constants can be also, hexadecimal (base 16) or binary (base 2). 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 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 into its 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. Values of nullable types cannot be implicitly converted into the appropriate value type, because the null value could be lost. Therefore, you need an explicit conversion.

The use of byte, sbyte, short, and ushort (instead of int) types can lead to non-obvious errors and makes sense only in exceptional cases.

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

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

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 is determined by the 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 the standard numeric, logical and comparison operators available in C++. These standard operators have the same precedence and associativity in C# as they do in C++.

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

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

Starting with C# 7.2, the conditional operator can be used to obtain 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 a 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, the so-called null-coalescing 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 is evaluated to 2
Console.WriteLine(m); // 2
m ??= 3; // m is not assigned 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 (subscipt)
postfix increment and decrement
creation of objects
getting type information
turning overflow checking

(x) 
x.y 
x?.y 

a[i] 
x++, x-- 
new 
typeof 
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 << >>
relational operators < > <= >= 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 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 an 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 and Program Flow Control

A statement is the smallest independent unit of program 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 a scope. An identifier declared inside a block has a scope from the point of definition to the closing brace. Blocks can be nested in each other.

A selection statement is either a conditional statement or a 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 control passes 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 execution and based on the following scheme:

switch (expression)
    block

The block has the following form:

{
    case constant_1: statements; break;
    case constant_2: statements; break;
    // ...
    default: statements; break;
}

There are some differences in usage of switch statement in C# and C++. In C#, you must use either 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 evaluate to an integer or string value. 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 the value of variable has been determined in some way:

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

Now we want to 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 more easily:

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 breaking out 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

An additional foreach loop is used for traversal of arrays and collections.

2.5 Working with Arrays

2.5.1 One-Dimensional Arrays

C# arrays are reference types. As in C++, arrays are zero-indexed. When declaring an array, square brackets are placed after the type name rather than the variable name:

int[] a;

This example shows description of a reference to an array that will be created later. The array length is not part of the array type. This allows an array of the required size to be defined at runtime:

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

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 eventually become eligible for garbage collection.

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. For integers and real numbers, it is 0; for Boolean values, it is false.

Note: the latest versions of C# allow you to create arrays of fixed length (inline arrays); work with such arrays will be considered later.

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

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

You can access particular elements using indexing. Indexing is always starting from zero. A typical loop for traversing an array is as follows:

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

When creating an array, the initial values ​​of its elements can be specified explicitly. To do this, an initialization list enclosed in curly braces is used:

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 };

Arrays are read from the keyboard item by item. The following example demonstrates reading the number and values of elements from the keyboard.

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 the element type can be inferred implicitly. 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 the creation of 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. Element indexing starts at zero and goes up to a value one less than the array size. 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; i++)
{
    a[i] = i;
}

foreach loop simplifies traversal of array elements:

foreach (type variable in array)
    loop 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. You also cannot get the element index within the body of a foreach loop.

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 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 index-from-end literals 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 its starting and ending positions, 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 a subsequence from a source array. In the following example, we create a new array containing copies of the selected 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.5.2 Multidimensional Arrays

There are two kinds of multidimensional arrays in C#:

  • ordinary multidimensional arrays in which all rows have the same length;
  • jagged arrays, which are arrays of arrays.

When declaring a standard multidimensional array, you must place square brackets after the type name, containing commas – one fewer than the number of dimensions:

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 similarly to Pascal:

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 rows can have different lengths. To declare jagged arrays, you must specify as many pairs of empty square brackets as there are dimensions in the array.

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 the sum of the elements of jagged array. Suppose we need to determine the sum of the elements of a jagged array. We can suggest the following program:

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.6 Functions, Methods, and Lambda Expressions

2.6.1 Functions. Methods

There are no global functions in C#. Instead of them, static methods of a class are used. Definition of a static method in the simplest case is as follows:

static result_type method_name(list_of_formal_parameters) body

In accordance with C# language style, function names begin with an uppercase letter.

Parameters (arguments) of function specified in the parameter list are called formal parameters. Parameters, which are used by invocation of a function, are called actual parameters or arguments. 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 integers:

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

We can omit the variable c:

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

Calling static methods of the current class from other methods can be done in an expression, in the description, or in the body of another function. When calling a function, its name and a list of actual parameters are specified without indicating their types. Actual parameters can be constants, variables, or expressions of appropriate types:

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

By default, the parameters are passed by value: the values of the actual parameters are copied into the memory cells created for the formal parameters. Passing parameters by reference using modifiers ref and out will be discussed later.

Starting from C# 7.0, you can create local functions. Now methods can be created inside the context of another method. A local function can be called only from the context in which it is declared.

If your program only contains a Main() method body without explicitly creating a class, all functions that are added in the code are local functions of a Main() method. The definition of a local function can be located both before and after its call:

double a = 3;
Print(a);

void Print(double x) // local function
{
    Console.WriteLine(x);
}

double b = 4;
Print(b);

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 the value of n (a 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;
    }
}

These rules also apply to the case when local functions are located directly in the body of a Main() method.

2.6.2 Calling Methods Defined in Other Classes

Methods created in other classes can be static or non-static.

Static methods do not require prior creation of the object. The required data is passed as parameters. Static methods are called using the class name, followed by the method name, separated from the class name by a dot. For example, the class Console provides methods Write() and WriteLine() for outputting results to the console without moving to a new line and with a newline, respectively. These methods are static, so they are called via the class name: Console.Write() and Console.WriteLine().

Calling non-static methods of a certain class requires the prior creation of an object of that class.

The use of static and instance methods can be illustrated using the System.Array class. This class is the base class for all arrays. This class provides methods for creating, processing, searching and sorting arrays.

The static method Fill() allows you to fill the previously created array with the specified value:

int[] b = new int[4];
Array.Fill(b, 1); // 1 1 1 1

This is equivalent to the following traditional code:

int[] b = new int[4];
for (int i = 0; i < b.Length; i++)
{
    b[i] = 1;
}

To copy an array, you can use static Copy() method of System.Array class. This method is implemented in two forms:

// 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);

The following example demonstrates both approaches.

// copying an array
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}

The static Array.Reverse() method arranges the elements of the array in reverse order. For example:

int[] a = { 1, 2, 3, 4 };
Array.Reverse(a); // 4 3 2 1

The static Array.Resize() method 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
}

The static Array.Sort() method with one parameter sorts array elements in ascending order:

int[] a = { 4, 2, 3, 1 };
Array.Sort(a); // 1 2 3 4

For arrays that contain numbers, you can find the sum, arithmetic mean, maximum, and minimum element. The corresponding non-static methods are called for the previously created array:

int[] a = { 4, 2, 3, 1 };
Console.WriteLine(a.Sum());     // 10
Console.WriteLine(a.Average()); // 2.5
Console.WriteLine(a.Max());     // 4
Console.WriteLine(a.Min());     // 1

You can add a new item to the array using the Append() method. The Concat() method allows you to add another array. The Except() method allows you to find and delete all occurrences of another array. These methods return an object of type IEnumerable from which an array can be obtained:

a = a.Append(10).ToArray(); // 4 2 3 1 10
int[] b = { 2, 3 }; 
a = a.Concat(b).ToArray();  // 4 2 3 1 10 2 3
a = a.Except(b).ToArray();  // 4 1 10

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

2.6.3 Lambda Expressions

The C# language, like most modern programming languages, supports defining functions using special expressions.

A lambda expression in C# has the following syntax:

  • a comma-separated list of formal parameters enclosed in parentheses; if the parameter is one, the parentheses may be omitted; if there are no parameters, an empty pair of parentheses is required;
  • arrow (=>);
  • a body consisting of one expression or a block; if a block is used, the return statement may be inside it.

For example, this is a function with one parameter:

k => k * k

The same with the brackets and the block:

(k) => { return k * k; }

Function with two parameters:

(a, b) => a + b

Function without parameters with the result of type void:

() => Console.WriteLine("Hello, World!")

Lambda expressions are used to create anonymous functions if the syntax requires a reference to a function that corresponds to a particular delegate. Delegates are references to functions (methods), they are an improved analogue of C++ function pointers.

The Array class provides a set of methods whose parameters are of type standard delegates. You can use lambda expressions. For example, we search for even numbers and sort in reverse order:

int[] arr = { 1, 10, 2, 14, 7 };
int[] result = Array.FindAll(arr, k => k % 2 == 0);  // 10 2 14
Array.Sort(arr, (m, n) => n.CompareTo(m));           // 14 10 7 2 1

You can also use the methods ForEach(), FindIndex(), etc.

Lambda expressions can also be used to simplify the implementation of constructors, overloaded methods, and properties.

2.6.4 Arrays as Parameters

Arrays are reference types. Therefore, when a reference to an array is passed to a function, operations are actually performed on the array for which the function was called:

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

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

To create functions with an arbitrary number of arguments, params modifier followed by array-type argument is used. Such a parameter must be the last parameter in the method's parameter list. 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 arguments 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.7 Strings

2.7.1 Overview

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

A string object can be created by assigning a string literal to a reference:

string s = "First string";

String constant may be preceded by the @ (et) character. These are so-called verbatim strings. Processing these strings involves ignoring escape sequences such as \t, \n, \\, \" and so on. Such constants are useful for determining file paths, 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, the other operand is converted to its string 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. As with other reference types, an array stores references to the strings rather than the strings themselves. 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 iterate through 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 containing the individual words of the source string. For example:

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

In the example above, the separator is a space between words. You can also define an array of separator characters.

The class System.String provides a number of useful static methods for creating and manipulating strings0. For example, the method Join() allows you to convert array items to strings and concatenate them. The first parameter of the method is a delimiter (character or string), the second parameter is the array whose elements need to be concatenated:

int[] arr = { 1, 2, 3 };
string s = string.Join("  ", arr); // two spaces as separator
WriteLine(s); // "1  2  3"

The Concat() method concatenates the elements of an array without a separator:

s = string.Concat(arr); // "123"

The Format() method performs string formatting:

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.

Strings can be checked for equivalence with the == operator.

Starting with C# 6.0, it became possible to get the name of a variable or type as a string using the nameof() operation, for example:

int count = 2;
string name = nameof(count);
Console.WriteLine(name); // count

2.7.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 a string object. This class is defined in the namespace System.Text. You can create a StringBuilder object 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 methods for modifying its contents. These methods are Append(), Remove(), Insert(), Replace(), etc. Consider the use of these methods in the following example:

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 program's efficiency when a specific string undergoes multiple modifications within the program. But it is important to remember that multiple references point to one object of type StringBuilder. So when we change it, all references will point to the changed string.

2.7.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 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 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.8 Console Output and Input

The System.Console class is used for console output and input. The Write() method writes the specified data at the current cursor position in the console window. In the simplest case method accepts a parameter of arbitrary type. The WriteLine() method appends a newline character to the output string. Calling WriteLine() without parameters moves the cursor to the next line.

As the first parameter of Write() and WriteLine() methods you can specify the output format string. The indices of subsequent parameters are specified in curly braces. In fact, the values are inserted into the output string at the specified locations. These values ​​are listed with commas. 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.

There is a problem with the incorrect display of certain Ukrainian characters in the console window. Adding a line with the following content to the source code will resolve this problem.

Console.OutputEncoding = System.Text.Encoding.UTF8;

The previous statement specify the use of the UTF-8 character encoding for output.

To enter data, ReadLine() method of Console class is used. This method returns a string that can be converted into the required number using the static Parse() method that 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 potentially 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 an integer value of x from the keyboard and calculates y according to the following table:

x
y
1
12
2
14
other values
16

The program (based on C#7) could look like this:

using System;

namespace Switcher
{
    class Program
    {
    
        static void Main()
        {
            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 essentially simplified using new features of C# 8 (switch expression) and C# 9 (an implicit class containing the Main() method):

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

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

double a = double.Parse(Console.ReadLine() ?? "0");
double b = double.Parse(Console.ReadLine() ?? "0");
ReplaceWithArithmeticMean();
Console.WriteLine("a = {0} b = {1}", a, b);

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

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

3.3 Use of Nullable Types

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

static double? Reciprocal(double x)
{
    if (x == 0)
    {
        return null;
    }
    return 1 / x;
}

Console.Write("Enter x: ");
double x = double.Parse(Console.ReadLine() ?? "0");
double? y = Reciprocal(x);
Console.WriteLine(y + "" ?? "Error");

3.4 Product of Numbers Entered from the Keyboard

In the following program, integers are read from the keyboard and appended to the array. Input terminates when zero is entered. This value is not appended to the array. Then we calculate the product of the elements.

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.5 Using Two-Dimensional and Jagged Arrays

Assume that we need to develop a program in which a two-dimensional array of real numbers is declared and initialized, and a jagged array whose rows contain positive elements copied from the appropriate rows of the first array is also created.

The following program first initializes a two-dimensional array, then creates an array of references to the rows of the jagged array. For each row of the first array, the number of positive elements is calculated and rows of the jagged array of the appropriate length are created. The positive elements of the rows of the first array are written into the created rows.

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.6 Sum of Digits

The sum of the digits can be calculated using string representation of a given integer:

String n = Console.ReadLine() ?? "";
int sum = 0;
for (int i = 0; i < n.Length; i++)
{
    sum += int.Parse(n[i] + "");
}
Console.WriteLine(sum);

3.7 Removing Unnecessary Spaces

The following program removes unnecessary spaces from the string (leaving only one space).

string s = Console.ReadLine() ?? "";
while (s.IndexOf("  ") >= 0)
{
    s = s.Replace("  ", " ");
}
Console.WriteLine(s);

For example, if you enter the following string

To    be  or not to                be

you'll obtain the following result:

To be or not to be

3.8 Working with an Array of Book Titles

Suppose we need to develop а C# console application for working with an array containing the titles of books on a bookshelf. The program should search and sort according to the following criteria:

  • we find titles that contain the sequence of letters "The";
  • sort case-insensitively in alphabetical order.

We can implement this task in two ways:

  • using traditional language constructs;
  • using methods of the Array class and lambda expressions.

The first way:

string[] bookTitles = {
    @"The UML User Guide",
    @"Pro C# 2010 and the .NET 4 Platform",
    @"Thinking in Java",
    @"Design Patterns: Elements of Reusable Object-Oriented Software",
    @"C# 9.0 in a Nutshell: The Definitive Reference"
};

void printTitles()
{
    foreach (var title in bookTitles)
    {
        Console.WriteLine(title);
    }
}

Console.WriteLine("\nInitial state:");
printTitles();
Console.WriteLine("\nTitles that contain \"The\"");
foreach (var title in bookTitles)
{
    if (title.Contains("The")) {
        Console.WriteLine(title);
    }
}

// Bubble sorting
bool mustSort;// repeat until mustSort true
do
{
    mustSort = false;
    for (int i = 0; i < bookTitles.Length - 1; i++)
    {
        if (string.Compare(bookTitles[i].ToUpper(), bookTitles[i + 1].ToUpper()) > 0)
        {
            // Swap items:
            string temp = bookTitles[i];
            bookTitles[i] = bookTitles[i + 1];
            bookTitles[i + 1] = temp;
            mustSort = true;
        }
    }
}
while (mustSort);
Console.WriteLine("\nAlphabetically without regard to case:");
printTitles();

The second way:

string[] bookTitles = {
    @"The UML User Guide",
    @"Pro C# 2010 and the .NET 4 Platform",
    @"Thinking in Java",
    @"Design Patterns: Elements of Reusable Object-Oriented Software",
    @"C# 9.0 in a Nutshell: The Definitive Reference"
};

Console.WriteLine("\nInitial state:");
Console.WriteLine(string.Join("\n", bookTitles));
Console.WriteLine("\nTitles that contain \"The\"");
string[] result = Array.FindAll(bookTitles, s => s.Contains("The"));
Console.WriteLine(result.Length > 0 ? string.Join("\n", result) : "No");
Console.WriteLine("\nAlphabetically without regard to case:");
Array.Sort(bookTitles, (s1, s2) => string.Compare(s1.ToUpper(), s2.ToUpper()));
Console.WriteLine(string.Join("\n", bookTitles));

The results should be identical.

4 Exercises

  1. Create a one-dimensional array with an even number of items. Split this array into two halves. Use features of the System.Range type.
  2. Define a one-dimensional array, enter the values of its items from the keyboard and add them to the array. The process is completed by entering zero. Output the sum of items.
  3. Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with ones, and negative values with zeros.
  4. Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with average of all items.
  5. Enter sentence and display all its words in separate lines.
  6. Enter sentence, concatenate all its words and display result.

5 Review Questions

  1. What are the programming paradigms?
  2. What are the reasons for the object-oriented approach?
  3. What are the components of object-oriented methodology?
  4. Name the basic principle and three basic concepts of OOP.
  5. What are the features of the .NET platform?
  6. How do value types differ from reference types?
  7. What is relationship between C# built-in types and standard CLR types?
  8. What are the benefits of using checked/unchecked blocks?
  9. What are advanced features of C# switch?
  10. Why use the Index type?
  11. What are the features of multidimensional arrays in comparison with C++?
  12. What is specific in creation and initialization of jagged arrays?
  13. What is the difference between a static method call and a non-static method call?
  14. What are the features of creating and calling local functions?
  15. What are lambda expressions and what are they used for? What are the standard methods for working with arrays?
  16. When we use function arguments with params attribute?
  17. What are verbatim strings and where they should be applied?
  18. How to modify contents of previously created string?
  19. How to modify particular character within string object?
  20. What are advantages and disadvantages of StringBuilder class versus String class?
  21. How to format data output?

 

up