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 that creates an array of strings according to one of tasks listed in the table. Search and sort array according to the given criteria:

Data stored in an array of strings Search criteria Sorting criteria
Surname of the student By the first letter of the surname By length
Name of the subject By the sequence of letters, taking into account the case By the number of words in the name
The name of the ward of the city By the last letter of the name By length
Name of the city The length of the name is greater than the specified value Alphabetically
Surname of the participant of the sports section The length of the surname is less than the specified value 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 group The length of the surname is greater than the specified value Alphabetically in reverse order
The title of the album of the music band The number of words in the title is more than specified 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 the room in the apartment By a certain number of letters Alphabetically in reverse order
The title of the story in the Storybook By the sequence of letters, taking into account the case By the number of words in the title
Name of the artist's work By the presence of a certain word By the number of words in the title in reverse order
The name of the metro station By the sequence of letters, taking into account the case Alphabetically
The name of the 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 permission with the teacher.

Give two implementations:

  • using traditional language structures (branching, cycles, etc.);
  • using methods of Array class and lambda expressions.

Do not use

  • LINQ 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, which defines the functions that calculates the square root of its argument x. The function returns double?. This function should return null, if it is impossible to calculate the root, or square root of an argument. Implement testing for different arguments.

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() and Math.Pow().

1.3 Greatest Common Divisor

Create a console application in the Main() function, which has a local function (with the result type void) for calculation of the greatest common divisor of two positive integer numbers by the simplest Euclidean algorithm. 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 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 Nygård.

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.1.9 Basic Principles of the OOP. Design Patterns

The effectiveness of the application of the 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, ensures the creation of more high-quality, flexible and reusable code. The most popular set of principles is five principles of SOLID:

S Single responsibility principle A class should have only one reason to change
O Open/closed principle Classes should be open to extension, but closed for change
L Liskov substitution principle Objects can be replaced by their descendants without changing the code
I Interface segregation principle There should be many specialized interfaces instead of one universal
D Dependency inversion principle Abstractions should not depend on the details, the details must 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 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 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 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 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# 13.0 (supported by last versions of Visual Studio 2022).

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 choose 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 (///).

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 option Documentation file ; you can also specify the name of the file in which the documentation will be generated ( XML documentation file path ).

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 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++, 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. Types can be

  • by value types: data is stored directly in the variable that was created (for example, in the call stack, if it is a local variable);
  • by 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 lists. Reference types are classes, interfaces, records, and delegates.

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 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 C++. 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 Working with Arrays

2.5.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.

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

2.6.1 Functions. Methods

There are no global functions in C#. 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

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

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

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. The local method can be only called from the context in which is it declared.

If your program only contains a Main() method body without explicit creating a class, all functions that are added in the code are local method 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 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;
    }
}

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 transition to a new line and with transition to a new line, 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 call of static and non-static methods can be considered on the example of 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() 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}    

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 a function definition mechanism through 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 brackets can 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

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.7 Strings

2.7.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.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 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:

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. 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() 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 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 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

The following program shows the working of the local function that replaces values of two variables with their arithmetic means. It uses 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 code of this file is implicit body of the Main() function.

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 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. Reading terminates with entering zero, which 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 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.

The following program first initializes a two-dimensional array, then creates an array of references to the rows of the unaligned array. For each row of the first array, the number of positive elements is calculated and rows of the unaligned 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

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

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 (leave one).

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.8 Working with an Array of Book Titles

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

  • we find names that contain the sequence of letters "The";
  • sort alphabetically without taking into account the case.

We can implement this task in two ways:

  • using traditional language structures;
  • using methods of 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 even count of items. Split this array into two halves. Use features of the System.Range class.
  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 Quiz

  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