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
Arrayclass 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 (
#warningand#errorwith corresponding error lines); - define named regions of code for outline display (
#regionand#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 |
(x) 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) statement1else 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 = expressionswitch { 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 = bswitch {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 variablein array) loop body
For example:
int [] a = {1, 2, 3};foreach (int xin 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 itemin 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(Arrayfrom , Array to,int length);// Copy a specified number of elements from fromIndex into a new array, starting from toIndex position: public static void Copy(Arrayfrom ,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 xin 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
returnstatement 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 elemin 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 xin 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 = "nis " + 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 cin 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 wordin 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 = xswitch { 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 xin 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
Arrayclass and lambda expressions.
The first way:
string [] bookTitles = { @"The UML User Guide", @"Pro C# 2010 and the .NET 4 Platform", @"Thinkingin Java", @"Design Patterns: Elements of Reusable Object-Oriented Software", @"C# 9.0in a Nutshell: The Definitive Reference" };void printTitles() {foreach (var titlein bookTitles) { Console.WriteLine(title); } } Console.WriteLine("\nInitial state:"); printTitles(); Console.WriteLine("\nTitles that contain \"The\"");foreach (var titlein 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 tocase :"); printTitles();
The second way:
string [] bookTitles = { @"The UML User Guide", @"Pro C# 2010 and the .NET 4 Platform", @"Thinkingin Java", @"Design Patterns: Elements of Reusable Object-Oriented Software", @"C# 9.0in 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 tocase :"); Array.Sort(bookTitles, (s1, s2) =>string .Compare(s1.ToUpper(), s2.ToUpper())); Console.WriteLine(string .Join("\n", bookTitles));
The results should be identical.
4 Exercises
- Create a one-dimensional array with an even number of items. Split this array into two halves. Use features of
the
System.Rangetype. - 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.
- Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with ones, and negative values with zeros.
- Initialize two-dimensional array of doubles with a list of initial values, replace all zeros with average of all items.
- Enter sentence and display all its words in separate lines.
- Enter sentence, concatenate all its words and display result.
5 Review Questions
- What are the programming paradigms?
- What are the reasons for the object-oriented approach?
- What are the components of object-oriented methodology?
- Name the basic principle and three basic concepts of OOP.
- What are the features of the .NET platform?
- How do value types differ from reference types?
- What is relationship between C# built-in types and standard CLR types?
- What are the benefits of using
checked/uncheckedblocks? - What are advanced features of C#
switch? - Why use the
Indextype? - What are the features of multidimensional arrays in comparison with C++?
- What is specific in creation and initialization of jagged arrays?
- What is the difference between a static method call and a non-static method call?
- What are the features of creating and calling local functions?
- What are lambda expressions and what are they used for? What are the standard methods for working with arrays?
- When we use function arguments with
paramsattribute? - What are verbatim strings and where they should be applied?
- How to modify contents of previously created string?
- How to modify particular character within string object?
- What are advantages and disadvantages of
StringBuilderclass versusStringclass? - How to format data output?
