Laboratory Training 3

Generic Programming

1 Training Tasks

1.1 Individual Task

Expand the program, which was created in the previous labs, by converting entity class into generic class, so it could possible to use different types for representation of additional data (string, newly created structure, some class). Within the class, which represents group of entities, add the following features of sorting:

Entity The first feature of sorting The second feature of sorting
Student In alphabetical order of surnames By year of birth
Faculty In alphabetical order of names By the number of students
Specialty In reverse alphabetical order of titles By the number of students
Subject In alphabetical order of titles By the number of unsatisfactory grades
Ward In reverse alphabetical order of names By the number of inhabitants
City In alphabetical order of names By the number of inhabitants
Member In alphabetical order of surnames By age
Player In alphabetical order of names By experience
Singer In alphabetical order of surnames By experience
Album In alphabetical order of titles By year of creation
Song In reverse alphabetical order of titles By rating
Room In alphabetical order of names By area
Story In alphabetical order of titles By year of creation
Masterpiece In alphabetical order of titles By year of creation
Novel In reverse alphabetical order of titles By year of creation

Implement functions of saving data in XML format and loading data from XML document. To work with XML documents use serialization and deserialization. To store the entities in the group, use list instead of an array. Reproduce previous laboratory work tasks.

All classes except class with Main() function should be placed in a separate library. Add reference to this library to new console application. Test all created functions. Implement testing additional data representation in three ways:

  • using string
  • using the special structure (struct)
  • using a separate class.

Implement handling possible exceptions.

1.2 Working with Text Files

Develop a program that performs copying from one file to another file only strings whose length is less than some integer value.

1.3 Implementation of Serialization and Deserialization

Create classes Student and Academic group (with an array of students as a field). Create objects and perform their serialization into XML document and deserialization from XML document.

1.4 Creation of a Library that Provides Generic Functions for Working with Arrays and Lists

Create a static class with generic static methods that implement the following functionality:

  • swap of two groups of elements
  • swap of all neighbour elements (with even and odd indices)
  • insert elements of another array (list) in the specified location
  • replacement of some group with elements of other array (list).

Implement these functions for arrays and lists.

1.5 Creating Your Own Container

Create a generic class that represent one-dimensional array of elements whose index varies from a specified value From to a value To inclusive. These values can be both positive and negative. The class should contain the following elements:

  • private field: "plain" array (list)
  • indexer
  • read only properties From and To (you can store From and calculate To)
  • constructor with parameters from and to that creates an empty array
  • constructor with parameters from and some array (with params attribute)
  • overloaded operator of conversion to string (operator string)
  • method providing iterator that enables traversal of elements using foreach
  • method of adding a new element
  • method of removing the last element.

The Main() function should contain testing of all elements of the class.

You should implement two versions: based on an array and based on a list.

1.6 Working with Set

Enter count of elements that will be stored in a set of integers and the range of numbers. Fill this set with random values. Print elements of the set, sorted in ascending order.

1.7 Working with Associative Array

Enter a sentence and calculate the number of different letters in a sentence. Display all the different letters in alphabetical order.

1.8 Creating a "Flexible" Array (Advanced Task)

Create a generic class for representation of one-dimensional array that automatically expands when user accesses a nonexistent element. For example, if you create an empty array a, the first reference to the element a[n] (whether for reading or for writing) provide extensions of the array so that it contains n + 1 element with indices from 0 to n-th inclusive. If certain elements are there, they are retained, and an array complemented by new elements. If the element already exists, it accessed in common way.

Create a constructor without parameters, an indexer, a property that returns the index of the last element, method providing an iterator, and override ToString() method. Implement testing of all features.

2 Instructions

2.1 Exception Handling

Using exceptions handling mechanism is a very important part of programming on all modern object-oriented languages. Exceptions allow programmer to separate points where runtime errors occur from points of error handling. An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions.

To generate exception, throw operator is used. After the throw keyword, you should place object of System.Exception class or classes derived from it. These derived classes reflect the specifics of a particular program.

class SpecificException : Exception
{
}

The System.Exception class contains a number of properties that you can use to access information about the exception, including:

  • Message – text description of the error, defined as the constructor parameter when creating the exception object;
  • Source – name of the object or application that throws an exception;
  • StackTrace – sequence of calls that resulted in the error.

In most cases, an exception object is created at the point of throwing exception by using new operator, but sometimes an exception object can be created before. Typical throw statement might look like this:

void F()
{
    . . .
    if (/* error */) 
        throw new SpecificException();
    . . .
}

The function header does not specify types of exceptions thrown by this function. In the following example, the Reciprocal() function throws an exception in the case of division by zero.

class DivisionByZero : Exception 
{
}

class Test 
{
    public double Reciprocal(double x)
    {
        if (x == 0) 
        {
            throw new DivisionByZero();
        }
        return 1 / x;
    }
}

Unlike C++, C# does not allow the creation of exceptions of primitive types. Only instances of classes derived from Exception are allowed.

The try block contains exception-prone code:

double x, y;
. . .
try 
{
    y = Reciprocal(x);
}

A try block is followed by a sequence of one or more catch statements, or handlers, each of which handles a different type of exception. The catch block without brackets handles all other exceptions:

catch (DivisionByZero d)
{
    // handling exception
}
catch (SpecificException)
{
    // handling exception
}
catch
{
    // handling exception
}

As shown in the example, you can omit object identifier in the header of catch block, if only type is important.

Exceptions form an object hierarchy, so a particular exception might match more than one catch block. What you have to do here is put catch blocks for the more specific exceptions before those for the more general exceptions.

In some cases, exceptions handler cannot ultimately handle the exception and should submit it to outer handler. This can be done by using the throw statement:

catch (SomeEx ex) 
{
    // local exception handling
    throw (ex);  // rethrowing 
}

You can use throw without expression, if exception object does not specified:

catch (Exception) 
{
    // local exception handling
    throw;
}
  

After the last catch block you can place finally block. This code is always executed regardless of whether an exception occurred or not.

try 
{
    OpenFile();
    // other activities
}
catch (FileError f)
{
    // handling exception
}
catch (Exception ex)
{
    // handling exception
}
finally {
    CloseFile();
}

.NET defined standard exceptions. These classes are also descendants of Exception. One of the most frequently occurring standard exception is System.NullReferenceException, which is thrown when you try to access class members by reference, which equal to null. The System.IndexOutOfRangeException is thrown when you try wok with improper array index.

There are also inner .NET exceptions that signal a serious problem during the execution of the program and may arise in any part of your program. These are ExecutionEngineException (internal CLR error), StackOverflowException, OutOfMemoryException, etc. Typically, these exceptions are not caught.

Starting from version 6 of the C# language, you can add the so-called exception filters to catch statements. Filter expressions after when keyword determine when a given catch clause should be applied. If this expression is true, the catch clause executes normally. Otherwise, the catch clause is skipped. For example:

static void SomeFunc(int k)
{
    if (k == 1)
    {
        throw new Exception("First case");
    }
    if (k == 2)
    {
        throw new Exception("Second case");
    }
    throw new Exception("Other case");
}

static void Main(string[] args)
{
    int n = int.Parse(Console.ReadLine());
    try
    {
        SomeFunc(n);
    }
    catch (Exception ex) when (ex.Message.Contains("First")) // Exception filter
    {
        Console.WriteLine("Our case!");
    }
    catch
    {
        Console.WriteLine("Something else");
    }
}

If this expression is put into catch block's code, it means that an exception is processed and rethrown. Usage of exception filters assumes that expression is not processed at all. Exception filters can be also used for debugging.

2.2 Initial Information about Working with Files

2.2.1 Working with Text Files

As almost all universal programming languages, C# provides means of working with files and other streams. These facilities are described in the System.IO namespace. The classes in this namespace offer a number of methods for creation streams, reading, writing, etc. Streams for working with text are called character streams. Base classes of all character streams are TextReader and TextWriter. Derived classes StreamWriter and StreamReader, and their derivatives, provide work with text files.

The following program performs reading lines from a text file and writes them into another text file.

using System;
using System.IO;

namespace LabThird
{
    class Program
    {
        static void Main(string[] args)
        {
            using (StreamReader reader = new StreamReader("From.txt", Encoding.Default))
            {
                using (StreamWriter writer = new StreamWriter("To.txt"))
                {
                    string s;
                    while ((s = reader.ReadLine()) != null)
                    {
                        writer.WriteLine(s);
                    }
                }
            }
        }
    }
}

Creation of stream objects within using block causes automatic call of Dispose() methods, which in turn invoke Close() functions. The From.txt file must be placed into bin\Debug or bin\Release folder of project before execution (depending on solution configuration).

The ReadToEnd() method allows you to read the entire file to the end and puts entire contents into a single string. This function allows reduction of the previous program:

using System;
using System.IO;

namespace LabThird
{
    class Program
    {
        static void Main(string[] args)
        {
            using (StreamReader reader = new StreamReader("From.txt", Encoding.Default))
            {
                using (StreamWriter writer = new StreamWriter("To.txt"))
                {
                    string s = reader.ReadToEnd();
                    writer.Write(s);
                }
            }
        }
    }
}    

Classes BinaryReader and BinaryWriter allow you to work with binary streams. There are also so-called memory streams, StringReader and StringWriter, which allow you to use strings as input and output streams.

2.2.2 Working with Binary Files

.NET tools include classes to conveniently work with binaries. The BinaryWriter class provides functions for writing to a data file various built-in value types, as well as byte arrays and character arrays. The BinaryReader class provides methods for reading data of all these types from a binary file. In order to create the corresponding streams, we first create file streams (objects of FileStream class). If fileName is a string containing a file name, streams are created as follows:

FileStream outputStream = new(fileName, FileMode.Create);
FileStream inputStream = new(fileName, FileMode.Open);

Further, these streams are used to create objects of types BinaryWriter and BinaryReader classes:

BinaryWriter writer = new BinaryWriter(outputStream);
BinaryReader reader = new BinaryReader(inputStream);

There are a number of Write() methods of the BinaryWriter class designed to record data of various types. The object of BinaryReader class can read this data using the methods ReadInt16(), ReadInt32(), ReadInt64(), ReadDouble(), ReadDecimal() etc. The best way of writing strings is to convert the string into an array of characters. Before writing this array to the file, it is advisable to write its length. The storing of the string called s will look like this:

writer.Write(s.Length);
writer.Write(s.ToCharArray());

When reading from a file, you should first read the length and then the characters from the array that was saved:

int len = reader.ReadInt32();
s = new String(reader.ReadChars(len));

Custom types can be stored, for example, as strings. If you need to store the date and time (an object of the DateTime class), the best way is to get the representation in the form of a long integer using ToBinary() method. Then the date and time can be reproduced in the corresponding object by reading the number of long type from the file long and using the static DateTime.FromBinary() method.

In the following example, data of various types contained in an object of type Employee is recorded:

namespace BinaryFilesDemo
{
    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; } = "";
        public string Surname { get; set; } = "";
        public DateTime DateOfBirth { get; set; }
        public decimal Salary { get; set; }

        public override string ToString()
        {
            string s = "Id:\t\t" + Id
                + "\nName:\t\t" + Name
                + "\nSurname:\t" + Surname
                + "\nDate of Birth\t" + DateOfBirth.ToShortDateString()
                + "\nSalary\t\t" + Salary;
            return s;
        }

        public void WriteToFile(string fileName)
        {
            using (FileStream fs = new(fileName, FileMode.Create))
            {
                using (BinaryWriter writer = new(fs))
                {
                    writer.Write(Id);
                    writer.Write(Name.Length);
                    writer.Write(Name.ToCharArray());
                    writer.Write(Surname.Length);
                    writer.Write(Surname.ToCharArray());
                    writer.Write(DateOfBirth.ToBinary());
                    writer.Write(Salary);
                }
            }
        }

        public void ReadFromFile(string fileName)
        {
            using (FileStream fs = new(fileName, FileMode.Open))
            {
                using (BinaryReader reader = new(fs))
                {
                    Id = reader.ReadInt32();
                    int len = reader.ReadInt32();
                    Name = new String(reader.ReadChars(len));
                    len = reader.ReadInt32();
                    Surname = new String(reader.ReadChars(len));
                    DateOfBirth = DateTime.FromBinary(reader.ReadInt64());
                    Salary = reader.ReadDecimal();
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Employee employee = new()
            {
                Id = 1,
                Name = "John",
                Surname = "Smith",
                DateOfBirth = DateTime.Parse("1989/12/31"),
                Salary = 1000
            };
            Console.WriteLine(employee);
            employee.WriteToFile("employee.bin");
            employee = new();
            Console.WriteLine(employee);
            employee.ReadFromFile("employee.bin");
            Console.WriteLine(employee);
        }
    }
}

The program first creates an object of type Employee. Then we display the data of the created object and write it to a binary file. Next, we create a new empty object of Employee type and read data from the binary file. The object with the read data is displayed on the screen.

It should be remembered that working with binary files does not involve viewing and editing files outside of a special program that was created for reading and writing data.

2.3 Working With XML Documents

XML (eXtensible Markup Language) is a platform-independent way of structuring information. Because XML separates the content of a document from its structure, it is successfully used for the exchange of information. For example, XML may be used to transfer data between the application and the database or between databases having different formats.

Focusing on the use of XML is one of the most important features of .NET platform. In particular, XML documents are used to describe the assembly configuration, generating and storing documents, serialization, internal data representation of data-aware components, describing the elements of a graphical user interface, data transmission in Web-services, etc.

XML documents are always text files. The syntax of XML is similar to the syntax of HTML, which is used for marking up texts published over the Internet. XML language can also be applied directly to the markup text.

There are two standard approaches to working with XML documents in your program:

  • event-based document model (Simple API for XML, SAX) supports processing events concerned with particular XML tags
  • Document Object Model, DOM allows creation and processing of collection of nodes in memory.

Both approaches use concept of parser. Parser is an application program, which parses document and split it into tokens.

Document Object Model presents an XML document as a tree of nodes, where each node represents an element, attribute, or text data. The tree structure that the DOM creates will always correspond to the hierarchical structure of the XML document.

The XmlNode class is a root of classes, which represent XML data. The XmlDocument class extends functionality of XmlNode. This class supports work with the whole XML document, such as creation, reading, editing, storing, etc.

The node type is determined by reading XML document. XML DOM provides several node types:

  • Document (XmlDocument class)
  • DocumentFragment (XmlDocumentFragment class)
  • DocumentType (XmlDocumentType class)
  • EntityReference (XmlEntityReference class)
  • Element (XmlElement class)
  • Attr (XmlAttribute class)
  • ProcessingInstruction (XmlProcessingInstruction class)
  • Comment (XmlComment class)
  • Text (XmlText class)
  • CDATASection (XmlCDataSection class)
  • Entity (XmlEntity class)
  • Notation (XmlNotation class)

XML document in memory is represented as a tree of linked objects of different node types.

You can also use XmlReader class for reading XML data. However, this class provides only unidirectional access.

2.4 Using Serialization

Serialization is a process of saving (transfer) state of the object in the stream (file), in particular for transmission over the computer network. Serial data stored contains all the information necessary for reconstruction (or deserialization) state object for future use. This information can be saved in various formats. The most compressed serialization is built on binary format. However, the best approach that provides the necessary visibility is serialization in XML-file. You can automatically store the values of public properties (fields).

To store objects in an XML document, XmlSerializer class is used. To use this type, as well as file streams, you need to add the directives:

using System.Xml.Serialization;
using System.IO;

Assume definition of Student class:

public class Student
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string[] Grades { get; set; }
}    

Object can be created within code of a program:

Student student = new Student() 
{ 
    Name = "Frodo", 
    Surname = "Baggins", 
    Grades = new string[] { "B", "D", "C" } 
};    

In order to implement serialization, you need to create an object of XmlSerializer:

XmlSerializer serializer = new XmlSerializer(typeof(Student));
using (TextWriter textWriter = new StreamWriter("Frodo.xml"))
{
    serializer.Serialize(textWriter, student);
}    

The result of serialization is a new XML document with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Name>Frodo</Name>
    <Surname>Baggins</Surname>
    <Grades>
        <string>B</string>
        <string>D</string>
        <string>C</string>
    </Grades>
</Student>    

As can be seen from the contents of the file, each property corresponds to a separate tag, arrays and lists correspond to compound tag.

Deserialization is performed as follows:

XmlSerializer deserializer = new XmlSerializer(typeof(Student));
using (TextReader textReader = new StreamReader("Frodo.xml"))
{
    student = (Student)deserializer.Deserialize(textReader);
}    

Sometimes it is more convenient to use XML-attributes for the properties instead of individual tags. This can be achieved by a special attribute of C# language (C# attributes will be discussed later):

[System.Xml.Serialization.XmlAttributeAttribute()]

This attribute is placed immediately before the corresponding properties:

public class Student
{
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string Name { get; set; }
    
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string Surname { get; set; }

    public string[] Grades { get; set; }
}    

Now we get the following XML document:

<?xml version="1.0" encoding="utf-8"?>
<Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="Frodo" Surname="Baggins">
    <Grades>
        <string>B</string>
        <string>D</string>
        <string>C</string>
    </Grades>
</Student>

2.5 Creation and Use of Generics

Generic programming is a programming paradigm that aims to separate scalar data, data structures, and algorithms for data processing. Generic collections can store objects of various data types, and storage mechanisms do not depend on the types of data objects. Generic algorithms, in turn, should be constructed so that they do not depend on any data type or particular types of structures, which hold data. Generic programming is focused on finding commonality among these container classes and algorithms and the creation of types and functions of a higher level of abstraction.

Languages that do not support generics, solve this problem by using a pointer to any variable (in C), or a reference to a common base class (in Delphi Pascal, and the first version of C#). This approach is not convenient from the viewpoint of type safety. For example, in the first version of C# creation of a class that stores pair of objects of the same type required definition of two references to the object class:

public class Pair
{
    object First, Second;

    public Pair(object first, object second)
    {
        First = first;
        Second = second;
    }
  
}

Since object is the base class for all types, you can use, for example, this class to store a pair of strings:

Pair p = new Pair("Surname", "Name");    

This approach has some drawbacks:

  • To read objects, you need to use explicit type conversion:
  •     string s = (string) p.First; // Instead of string s = p.First;
  • You cannot be sure that the pair stores objects of the type you need:
  •     int i = (int) p.Second; // Runtime error
  • You cannot guarantee that both fields are the same type:
  •     Pair p1 = new Pair("Surname", 2); // No any error messages

In addition, storage of value types is associated with automatic packaging, which is not too efficient. Of course, you can create multiple classes to store pairs of different data types, such IntegerPair, FloatPair, StringPair, etc. This approach is bulky and uncomfortable. It is also difficult to find and correct errors.

Another problem is the creation of functions for working with arrays of different types. A lot of common algorithms (e.g., swap two items, change the order of items on the reverse) do not depend on the type of the array elements. As in previous case, you can use the object as type of elements. For example:

public static void SwapElements(object[] arr, int i, int j)
{
    object e = arr[i];
    arr[i] = arr[j];
    arr[j] = e;
}    

Again, we meet the same problems: lack of control types at compile time, "absurd" type conversion, and more. Creation of separate functions for different types is bulky and error-prone, sometimes, even impossible.

Starting with version 2, C# allows you to create and use generics – syntax constructs that include parameters, structures, or functions, which contain additional information about data types. These parameters are taken in angle brackets and separated by commas. In the particular case, the list contains one parameter.

Generics provide the ability to create and use type-safe syntactic constructs. The types whose description contains parameters are called generic. When creating an object of generic type, the names of real types are indicated in angle brackets. C# allows you to use any type.

Here is an example of the generics:

public class Pair<T>
{
    public T First  { get; set; }
    public T Second { get; set; }

    public Pair(T first, T second)
    {
        First = first;
        Second = second;
    }
  
}

class Program
{
    static void Main(string[] args)
    {
        Pair<string> p = new Pair<string>("Surname", "Name");
        string s = p.First; // Get string value without type casting
        Pair<int> p1 = new Pair<int>(1, 2); // You can use integer constants
        int i = p1.Second;  // Get integer value without type casting
    }
}    

Generic classes cannot contain Main() function.

If we try to add to a pair of different data types, the compiler generates an error. An attempt of explicitly type conversion is also erroneous:

Pair<string> p = new Pair<string>("1", "2");
int i = (int) p.Second; // Compile error

The data type with the parameter in angle brackets (e.g. Pair<string>) is called parameterized type.

Besides generic classes and structures, it is possible to create generic functions in both generic and non-generic classes (structures):

public class ArrayPrinter
{
    public static void PrintArray<T>(T[] a)
    {
        foreach (T x in a)
        {
            Console.Write("{0, 10}", x);
        }
        Console.WriteLine();
    }

    static void Main(string[] args)
    {
        string[] sa = {"First", "Second", "Third"};
        PrintArray(sa);
        int[] ia = {1, 2, 4, 8};
        PrintArray(ia);
    }
}    

The recommended names of formal parameters begin with a capital T.

Generic can have two or more parameters. In the following example, the pair can contain objects of different types:

public class PairOfDifferentObjects<TFirst, TSecond>
{
    public TFirst First  { get; set; }
    public TSecond Second { get; set; }

    public PairOfDifferentObjects(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }
}

class Program
{
    static void Main(string[] args)
    {
        PairOfDifferentObjects<int, String> p =
              new PairOfDifferentObjects<int, String>(1000, "thousand");
        PairOfDifferentObjects<int, int> p1 =
              new PairOfDifferentObjects<int, int>(1, 2);
        //...

    }
}    

By default, the generic type is considered directly or indirectly derived from object class. Sometimes this assumption is not enough to create useful classes and functions. For example, we cannot call methods other than ones defined in the object class, we cannot even create an object using the new operator, because we not sure that the type provides constructor without parameters. In order to extend the functionality of generic classes and methods, so-called limitations are used to describe generics. After generic, the where keyword is placed, and then you can indicate the presence of a constructor with no parameters, the base class, and interfaces implemented by type. For example:

class A<T> where T : new() // type T must provide a constructor with no parameters
{
    public T Data = new T(); 
}

interface Int1
{
    void doSomething();
}

class B<T> where T : Int1 // type T must implement the specified interface
{
    T data;
    public void f()
    {
        data.doSomething();
    }
}

class C<T> where T : class // type T must be reference type
{
}

class D<T> where T : struct // type T must be value type
{
}    

You can create derived generic classes, as well as non-generic classes, from generic base class. If you create non-generic class, you must specify the particular type of the parameter. For example:

class X<T>
{
}

class Y<T> : X<T>
{
}

class Z : X<int>
{
}    

You can also define generic overloaded operators.

You can also serialize objects of generic classes. In this case, the names of tags (attributes) automatically set by serializer to PropertyNameOfTypeName.

2.6 Standard Generic Classes and Methods

2.6.1 Overview

Container class is a class whose objects have the ability to store other objects or references. The simplest container is a regular array. The functionality of arrays is not sufficient for many tasks, so modern programming languages and platforms provide various opportunities for making containers.

C# provides variants of creating containers as using generics (namespace System.Collections.Generic), or without generics (the namespace System.Collections). Non-generic collections are considered obsolete and deprecated. System.Collections.Generic namespace connects to all automatically generated programs.

All standard containers implement the IEnumerable interface. The following table shows some generic interfaces and standard generic classes that implement these interfaces:

Interface Description Standard Classes that Implement the Interface
ICollection<T> generic collection Stack<T> (stack), Queue<T> (queue), LinkedList<T> (linked list) and all classes are listed below
IList<T> list List<T> (list constructed using the internal array)
ISet<T> set HashSet<T> (set based on hash table)
SortedSet<T> (sorted set)
IDictionary<K,V> dictionary (associative array) Dictionary<K,V> (dictionary built on Hash table)
SortedDictionary<K,V> (dictionary sorted by keys)

All classes provide a lot of static and non-static functions for working with sequences of elements. Containers can store both references and elements of value types (int, double, and so on).

The Stack class allows you to create a data structure organized on the principle of "last in first out" (LIFO). The Queue class provides a data structure organized on the principle of "first in first out" (FIFO).

2.6.2 Working with Lists

The IList interface describes an ordered collection (sequence). The generic class List, which implements the IList interface, represents a list created based on array (similar to vector class in C++). Like in arrays, access to the elements can be made by index (using [] operator). Unlike arrays, the size of the lists can be dynamically changed. The Count property returns a number of elements contained in the list. Like elements of the array, list items are numbered from zero.

Some methods of List class are listed below:

Method

Description

Add() Adds an object to the end of the list
AddRange() Adds the elements of the specified collection to the end of the list
Clear() Removes all items from the list
Contains() Determines whether the list contains specified element
CopyTo() Copies the list or its part into an array
IndexOf() Returns the index of the first occurrence of value in a list or in its part
Insert() Adds an item to the list at the specified position
InsertRange() Adds a collection of elements into the list at the specified position
Remove() Removes the first occurrence of the specified object from the list
RemoveAt() Removes the list item with the specified index
RemoveRange() Removes a range of elements from a list
Reverse() Changes the order of items in a list or in its part on reverse
Sort() Sorts the elements of the list or its part
ToArray() Copies the elements of the list into a new array

You can create an empty list of objects of a certain type (SomeType) by using the default constructor:

IList<SomeType> list1 = new List<SomeType>();

You can also directly define reference to List:

List<SomeType> list1 = new List<SomeType>();

The second option is less desirable, since it reduces flexibility of the program. The first approach makes it easy to replace the implementation of the List to any other implementation of IList interface, which is more consistent with the requirements of a particular task. In the second case, it is tempting to call methods specific to the List, so switching to another implementation will be difficult. For local variables, the description of the var keyword can be used. For example, the last definition could be as follows:

var list1 = new List<SomeType>();

You can create a list from an existing array using special constructor:

int[] a = { 1, 2, 3 };
IList<int> list2 = new List<int>(a);

You can create a new list using the existing one. The new list contains copies of items. For example:

var list3 = new List<int>(list2);

Like arrays, lists can be created using initializers:

IList<int> list4 = new List<int> { 11, 12, 13 };

After creation of empty list, you can add elements using Add() method. This method with a single argument adds an element to the end of the list:

IList<string> list5 = new List<string>();
list5.Add("abc");
list5.Add("def");
list5.Add("xyz");

You can add all the other elements of the list (or other collection) to this list using the Concat() method. The result is a new collection (an object of type IEnumerable<>):

var result = list3.Concat(list4); // 1 2 3 11 12 13

Lists allow storage of elements of types int, double, and so on. You can traverse over lists using foreach construct (like over arrays):

foreach (int i in list3)
    Console.WriteLine(i);    

If you add or remove items in random places more often than get items by index, it is advisable to use the class LinkedList<>, which stores objects using a linked list. This class also implements IList<> interface.

2.6.3 Use of Standard Generic Functions

Before you create your own generic classes and methods, you should look for standard ones. The System.Array class provides a large number of static generic functions for working with arrays, such as Resize() ( changing the size keeping existing elements), Reverse() (placing items in reverse order), Copy() (copying one array (or its part) into another array, or even into source array, from specified position), Sort() (sorting) and many others. These functions can be demonstrated in the following example:

using System;
using System.Collections.Generic;

namespace Arrays
{
    class Program
    {
        public static void Print<TElem>(IList<TElem> arr)
        {
            foreach (TElem elem in arr)
            {
                System.Console.Write("{0, 6}", elem);
            }
            System.Console.WriteLine();
        }

        static void Main(string[] args)
        {
            int[] a = { 1, 2, 3, 4 };
            Array.Resize(ref a, 5);
            Print(a);                // 1     2     3     4     0
            Array.Reverse(a);
            Print(a);                // 0     4     3     2     1
            Console.WriteLine(Array.IndexOf(a, 4)); // 1
            Array.Sort(a);           // 0     1     2     3     4
            Print(a);
            int[] b = new int[2];
            Array.Copy(a, b, b.Length);
            Print(b);                // 0     1
            Array.Copy(a, 2, b, 0, b.Length);
            Print(b);                // 2     3
        }
    }
}

This example shows that System.Array class implements the IList interface. Therefore, its instances can be used as parameters of IList type.

In contrast to arrays, appropriate methods of lists are non-static:

    static void Main(string[] args)
    {
        List<int> a = new List<int> { 1, 2, 3, 4 };
        a.Add(0);
        Print(a);                // 1     2     3     4     0
        a.Reverse();
        Print(a);                // 0     4     3     2     1
        Console.WriteLine(a.IndexOf(4)); // 1
        a.Sort();           
        Print(a);                // 0     1     2     3     4
        int[] b = new int[5];
        a.CopyTo(b, 0);          // copy into array, starting from the specified position
        Print(b);                // 0     1     2     3     4
    }    

Arrays and lists with numeric items are sorted in ascending order. The CompareTo() method of IComparable interface can be defined in classes and structures (that implement IComparable interface) for setting default sort order. This method should return a negative value (e.g. -1) if this object less than argument, 0 if objects are equal, and a positive value otherwise. The elements of arrays (lists) should be objects that implement IComparable interface. You can choose to create a class that implements IComparable interface. For example, an array of rectangles can be sorted by area:

class Rectangle : IComparable<Rectangle>
{
    double width, height;

    public Rectangle(double width, double height)
    {
        this.width = width;
        this.height = height;
    }

    public double Area()
    {
        return width * height;
    }

    public int CompareTo(Rectangle rect)
    {
        return Area().CompareTo(rect.Area());
    }

    public override String ToString()
    {
        return "[" + width + ", " + height + ", area = " + Area() + "]";
    }
  
}

class Program
{
    static void Main(string[] args)
    {
        Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)};
        Array.Sort(a);
        foreach (Rectangle rect in a)
            Console.WriteLine(rect);
    }
}

If you attempt to sort items that do not implement the IComparable interface, you obtain InvalidOperationException.

If you do not want (or cannot) determine the CompareTo() function, you can create a separate class that implements the interface IComparer. Reference to this class object are passed as the second parameter in the function Sort() for arrays (or the first option for lists). IComparer interface the method Compare() with two parameters. The function should return a negative number if the first object should be considered less than another, zero if objects are equivalent, and a positive number otherwise.

class Rectangle
{
    double width, height;

    public Rectangle(double width, double height)
    {
        this.width = width;
        this.height = height;
    }

    public double Area()
    {
        return width * height;
    }

    public override String ToString()
    {
        return "[" + width + ", " + height + ", area = " + Area() + "]";
    } 
}

class CompareByArea : IComparer<Rectangle>
{
    public int Compare(Rectangle r1, Rectangle r2)
    {
        return r1.Area().CompareTo(r2.Area());
    }
}

class Program
{
    static void Main(string[] args)
    {
        Rectangle[] a = { new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4) };
        Array.Sort(a, new CompareByArea());
        foreach (Rectangle rect in a)
        {
            Console.WriteLine(rect);
        }
    }
}

Lists can be sorted in analogous way.

2.6.4 Working with Sets

Set is a collection that does not contain the same elements.

The HashSet class uses so-called hash codes to identify items. Hash codes provide fast access to data on some key. All C# objects can generate hash codes. Hash code is a sequence of bits of fixed length. For each object, this sequence is unique. SortedSet class uses a binary tree to store items and guarantee them a certain order.

The Add() method adds an element to the set and returns true if the element was previously not present in a set. Otherwise, the item cannot be added, and Add() returns false. The Remove() method removes the specified element set, if any. All elements of the set can be removed using the Clear() method. For example:

using System;
using System.Collections.Generic;

namespace SetDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedSet<String> set = new SortedSet<string>();
            Console.WriteLine(set.Add("one"));      // True
            Console.WriteLine(set.Add("two"));      // True
            Console.WriteLine(set.Add("three"));    // True
            Console.WriteLine(set.Add("one"));      // False
            foreach (string s in set)
            {
                Console.Write("{0} ", s);     // one three two
            }
            Console.WriteLine();
	          set.Remove("three");
	          foreach (string s in set)
            {
	              Console.Write("{0} ", s);     // one two
            }
	          Console.WriteLine();
	          set.Clear();
        }
    }
}    

As can be seen from the above examples, the same elements cannot be added twice. Items are automatically recorded in ascending order.

Elements can be accessed by index. The Contains() method returns true, if the set contains the specified element. Count property returns the number of elements. In addition, sorted sets have properties that return the minimum and maximum values (Min and Max). You can copy elements of the set into one-dimensional array using CopyTo().

In the following example, ten random values in the range from -9 to 9 are added to the set of integers:

using System;
using System.Collections.Generic;

namespace SetOfIntegers
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedSet<int> set = new SortedSet<int>();
            Random random = new Random();
            for (int i = 0; i < 10; i++)
            {
                int k = random.Next() % 10;
                set.Add(k);
            }
            foreach (int k in set)
            {
                Console.Write("{0} ", k); ;
            }
        }
    }
}

The resulting set usually contains less than 10 numbers because some values may be repeated. Since SortedSet is used, numbers are stored and displayed in an ordered (ascending) form. To add exactly ten different numbers, you can modify the program, for example, using while loop instead for loop:

while (set.Count < 10)
{
    . . .
}

Set can only contain different elements, therefore it can be used to calculate different words, letters, numbers, etc. You can create set and call Count property. Using SortedSet, you can display words and letters in alphabetical order.

2.6.5 Associative Arrays

Associative arrays store pairs of objects (references). Associative arrays are also generic types. Associative arrays in C# are represented by generic interface IDictionary, which is implemented by classes Dictionary and SortedDictionary. The last class stores pairs sorted by key. Keys, unlike values, cannot be repeated.

Each object (value) is stored in an associative array associated with another object (key).The Add(key, value) method adds value and associate it with the key. If associative array still contains a pair with the specified key, the new value replaces the old one. To check presence of a key and a value, ContainsKey() and ContainsValue() methods are used. You can traverse over associative array by using the iteration on the elements of type KeyValuePair with Key and Value properties. You can access both existing and missing elements using indexer. The Remove() method allows you to delete item associated with particular key.

Keys and values can be both different and similar types. For example:

using System;
using System.Collections.Generic;

namespace DictionaryTest
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedDictionary<string, string> dictionary = new SortedDictionary<string, string>();
            dictionary.Add("sky", "небо");
            dictionary.Add("house", "дiм");
            dictionary.Add("white", "бiлий");
            dictionary.Add("dog", "собака");
            dictionary["dog"] = "собака";    // the same as dictionary.Add("dog", "собака");
            foreach (var pair in dictionary) // pair of KeyValuePair type
            {
                Console.WriteLine("{0}\t {1}", pair.Key, pair.Value); // output in alphabetical order
            }
            Console.WriteLine(dictionary.ContainsValue("небо"));    // True
            Console.WriteLine(dictionary.ContainsKey("city"));      // False
            dictionary.Remove("dog");
            dictionary["house"] = "будинок";
            foreach (var pair in dictionary)
            {
                Console.WriteLine("{0}\t {1}", pair.Key, pair.Value);
            }
        }
    }
}

The Keys and Values properties return collections of keys and values accordingly.

Version 6 of the C# provides a new way of initialization for dictionaries – a special form of an index initializer. For example:

Dictionary<string, string> countries = new Dictionary<string, string>
{
    ["France"] = "Paris",
    ["Germany"] = "Berlin",
    ["Ukraine"] = "Kyiv"
};
foreach (var pair in countries)
{
    Console.WriteLine("{0}\t {1}", pair.Key, pair.Value);
}

2.7 Creating Your Own Container Types

Despite the great deal of standard container classes, sometimes there is a need to create our own containers. There may be, for example, complex trees, more flexible lists, collections of special items, etc. Most of these containers are created based on existing types (collections or arrays). We can access individual items by adding indexers. However, sometimes it also can be convenient traverse elements of container in the foreach loop. The use of such cycles is possible if the container class provides the so-called iterator. Iterator is helper object that provides traversal through the collection.

To provide an iterator, container class must implement the GetEnumerator() method. Within this method, a special keyword yield is used. The yield return statement returns one element of the collection and the current pointer shifts to the next element. In the following example, the iterator traverses the individual letters of the word:

using System;
using System.Collections.Generic;

namespace LabThird
{
    class Letters
    {
        string word;

        public Letters(string word)
        {
            this.word = word;
        }
    
        public IEnumerator<char> GetEnumerator()
        {
            foreach (char c in word)
            {
                yield return c; 
            }
        }
    }
  
    class Program
    {
        static void Main(string[] args)
        {
            Letters letters = new Letters("Hello!");
            foreach (char c in letters)
            {
                Console.WriteLine(c); 
            }
        }
    }
}    

After starting the program, we obtain the following result:

H
e
l
l
o
!    

2.8 Creation and Use of Class Libraries

The concept of the library is common for different programming languages, software platforms and software in general. A library is a collection of resources used during the development and implementation of programs. A specific set of resources depends on what it is used for the library.

At the development time programmers use libraries that provide classes, objects, functions, sometimes variables, and other data. These libraries are provided in the form of source code or compiled code. Different applications can use these resources statically (code of library functions and other resources included into one executable file) or dynamically (file with machine code must be present at runtime). Dynamic libraries same library can be simultaneously used by several applications.

The .NET platform supports the mechanism of dynamic libraries. The standard classes are grouped in assemblies and compiled into dynamic-link libraries. These files usually have the extension dll.

When you create a new application in MS Visual Studio, the most common standard libraries of .NET are automatically connected to your project.

Sometimes you need to create your own class libraries. These libraries group namespaces and classes, which can then be used in other projects. In the simplest case, these libraries will be used in the projects of the current solution. To add a new library to the existing solution, choose the Solution branch in the Solution Explorer, and add a new project (Add | New Project...) via the context menu. It will be Class Library. In the right part of the window, select the Class Library (.NET Core) template. Next, enter the name of the library. It is advisable to a file created by default (Class1.cs) according to the content of classes that will be added to the library. If you now want to use the classes of this new library, you must connect it manually to each project, where it is needed (Add | Project Reference...).

3 Sample Programs

3.1 Creation of a Library that Provides Generic Methods

Despite the huge number of functions for working with arrays, which provides class System.Array, some useful functions are missing. For example, we could add functions of swapping elements, inserting an element at the specified location and output of the array to the console window. We create a namespace called Arrays and add a static class ArrayUtils. The corresponding functions are realized as generic static methods.

namespace Arrays
{
    public static class ArrayUtils
    {
        public static void SwapElements<TElem>(TElem[] arr, int i, int j) {
            // Copy one of the elements to a temporary cell:
            TElem elem = arr[i];
            arr[i] = arr[j];
            arr[j] = elem;
        }

        public static void Insert<TElem>(ref TElem[] arr, int index, TElem elem)
        {
            // Resizing array:
            System.Array.Resize(ref arr, arr.Length + 1);
            // Shift forward elements by copying:
            System.Array.Copy(arr, index, arr, index + 1, arr.Length - index - 1); 
            // Setting a new value:
            arr[index] = elem;
        }

        public static void Print<TElem>(TElem[] arr)
        {
            foreach (TElem elem in arr)
            {
                System.Console.Write("{0, 6}", elem);
            }
            System.Console.WriteLine();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int[] a = { 1, 2, 3, 4 };
            ArrayUtils.SwapElements(a, 2, 3);
            ArrayUtils.Print(a);                    // 1     2     4     3
            ArrayUtils.Insert(ref a, 2, 11);
            ArrayUtils.Print(a);                    // 1     2    11     4     3
            string[] b = { "one", "two", "three" };
            ArrayUtils.SwapElements(b, 2, 0);
            ArrayUtils.Print(b);                    // three   two   one
            ArrayUtils.Insert(ref b, 3, "zero");
            ArrayUtils.Print(b);                    // three   two   one  zero
        }
    }
}    

The Insert<TElem>() method gets the parameter of array type as a reference, since it creates a new array (i.e. changes value of reference).

3.2 Working with Set

In the following example, sentence entered from keyboard and then all the different letters (excluding separators) are in displayed in alphabetical order::

using System;
using System.Collections.Generic;

namespace Sentence
{
    class Program
    {
        static void Main(string[] args)
        {
            string sentence = Console.ReadLine();
            // Set of separators:
            HashSet<char> delimiters = new HashSet<char>() 
                    {' ', '.', ',', ':', ';', '?', '!', '-', '(', ')', '\"'};
            // Set of letters:
            SortedSet<char> letters = new SortedSet<char>();
            // Add all characters except delimiters:
            for (int i = 0; i < sentence.Length; i++)
            {
                if (!delimiters.Contains(sentence[i]))
                {
                    letters.Add(sentence[i]);
                }
            }
            foreach(char c in letters)
            {
                Console.Write("{0} ", c);
            }
        }
    }
}

3.3 Working with Associative Array

In the following example, the number of occurrences of the different words in a sentence is calculated. The words and the corresponding number are stored in the associative array. Usage of SortedDictionary class provides an alphabetical order of words (keys).

using System;
using System.Collections.Generic;

namespace WordsCounter
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedDictionary<string, int> d = new SortedDictionary<string, int>();
            string s = "the first men on the moon";
            string[] arr = s.Split();
            foreach (string word in arr)
            {
                int count = d.ContainsKey(word) ? d[word] : 0;
                d[word] = count + 1;
            }
            foreach (var pair in d)
            {
                Console.WriteLine(pair.Key + "\t" + pair.Value);
            }
        }
    }
}

3.4 Creating Our Own Container

Suppose we want to create a class to represent an array of elements whose index varies from one to the number of items (including this number). It is typical for languages such as Pascal and BASIC. In addition, we can extend the functionality of the array by overriding of ToString() method. We can also provide methods of adding a new element and removing the last element.

Such class should be generic. Plain array will be present as a field. In order to be able to traverse over elements of the new container using foreach, we need to define an iterator, namely, GetEnumerator() method. The source code of the program will be as follows:

using System;
using System.Collections.Generic;

namespace LabThirdArray
{
    // An array whose elements are indexed from one
    public class ArrayFromOne<TElem>
    {
        private TElem[] arr = { };

        // Indexer
        public TElem this[int index]
        {
            get { return arr[index - 1]; }
            set { arr[index - 1] = value; }
        }

        // Number of objects in the array
        public int Length 
        {
            get { return arr.Length; }
        }

        // Create an empty array of the specified length
        public ArrayFromOne(int maxIndex)
        {
            arr = new TElem[maxIndex];
        }

        // Create an object from "normal" array or list of items
        public ArrayFromOne(params TElem[] arr)
        {
            this.arr = new TElem[arr.Length];
            Array.Copy(arr, this.arr, arr.Length);
        }

        // Define an iterator for traverse using foreach
        public IEnumerator<TElem> GetEnumerator()
        {
            foreach (TElem x in arr)
            {
                yield return x;
            }
        }

        // Elements are separated by commas, the entire list enclosed in brackets
        public override string ToString()
        {
            string s = "[";
            foreach (TElem elem in arr)
            {
                s += elem + ", ";
            }
            // Remove the last space and comma:
            return s.Substring(0, s.Length - 2) + "]"; 
        }

        // Adding a new element to the end of the array
        public void Add(TElem elem)
        {
            Array.Resize(ref arr, Length + 1);
            this[Length] = elem;
        }

        // Remove the last element
        public void RemoveLast()
        {
            if (arr.Length > 0)
            Array.Resize(ref arr, Length - 1);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ArrayFromOne<int> a = new ArrayFromOne<int>(1, 2, 3);
            for (int i = 1; i <= a.Length; i++)
            {
                a[i]++;
            }
            Console.WriteLine(a);
            a.Add(8);
            Console.WriteLine(a);
            a.RemoveLast();
            foreach (int x in a)
            {
                Console.WriteLine(x);
            }
            ArrayFromOne<string> b = new ArrayFromOne<string>(1);
            b[1] = "one";
            b.Add("two");
            Console.WriteLine(b);
        }
    }
}

You can also implement a class, which field is not an array, but a list. Part of the code remains unchanged. You can also add a constructor with params argument, which initializes array of objects.

3.5 Creating a Hierarchy of Generic Classes

Suppose you want to expand the program of data processing about books on the Bookshelf by features for saving data in XML format and loading data from XML document. Working with XML-documents will be based on serialization mechanism.

The examples in the previous two labs have proposed various solutions for data presentation about the author – as a string and a separate structure. You can also provide other variants (structure or other types, classes, etc.). In order to combine all the possible implementations, we can use generic type for representation of author. Use of generic allows us to defer a decision data type for representation of author. Depending on particular needs, we'll be able to use custom classes, structures, standard strings, or something else.

Since multi-use of software expected, all classes that describe entities of our domain should be placed in a separate library. This allows us create a console application to test the classes first, and then use a library for creating graphical user interface program, or for Web-application.

First, create a library and console test. Create a new solution (Bookshelf) with console application. Then we add a new project (Class Library) to the solution (Add | New Project...). A name of a library would be BookshelfLib. Automatically created file (Class1.cs) should be renamed in BookshelfClasses.cs. This should be done using Properties window, selecting Class1.cs file in the Solution Explorer.

Then we manually add the following text:

using System.Xml.Serialization;

namespace BookshelfLib
{
    // Structure that represents an author
    public struct Author
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Surname { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Name { get; set; }

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj == null)
            {
                return false;
            }
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        // Definition of string representation:
        public override string ToString()
        {
            return Name + " " + Surname;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public static bool operator ==(Author left, Author right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Author left, Author right)
        {
            return !(left == right);
        }
    }

    // Book 
    public class Book<TAuthor>
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Title { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public int Year { get; set; }

        public List<TAuthor> Authors { get; set; }

        // Constructors
        public Book()
        {
            Title = "";
            Authors = new List<TAuthor>();
        }

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

        // Definition of string representation
        override public string ToString()
        {
            string s = string.Format("Title: \"{0}\". Year of publication: {1}", Title, Year);
            s += "\n" + "   Author(s):";
            for (int i = 0; i < Authors.Count; i++)
            {
                s += string.Format("      {0}", Authors[i]);
                if (i < Authors.Count - 1)
                {
                    s += ",";
                }
                else
                {
                    s += "\n";
                }
            }
            return s;
        }

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj is Book<TAuthor> b)
            {
                if (b.Authors.Count != Authors.Count)
                {
                    return false;
                }
                for (int i = 0; i < Authors.Count; i++)
                {
                    if (!b.Authors[i].Equals(Authors[i]))
                    {
                        return false;
                    }
                }
                return b.Title == Title && b.Year == Year;
            }
            return false;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

    }

    // Bookshelf
    public class Bookshelf<TAuthor>
    {
        public List<Book<TAuthor>> Books { get; set; }

        // Constructor
        public Bookshelf(params Book<TAuthor>[] books)
        {
            Books = new List<Book<TAuthor>>(books);
        }

        // Indexer
        public Book<TAuthor> this[int index]
        {
            get { return Books[index]; }
            set { Books[index] = value; }
        }

        // Definition of string representation
        override public string ToString()
        {
            string result = "";
            foreach (Book<TAuthor> book in Books)
            {
                result += book;
            }
            return result;
        }

        // Looking for books with a certain sequence of characters
        public List<Book<TAuthor>> ContainsCharacters(string characters)
        {
            List<Book<TAuthor>> found = new();
            foreach (Book<TAuthor> book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Adding a new element to a list:
                    found.Add(book);
                }
            }
            return found;
        }

        // Adding book
        public void Add(Book<TAuthor> book)
        {
            Books.Add(book);
        }

        // Removal of a book with the specified data
        public void Remove(Book<TAuthor> book)
        {
            Books.Remove(book);
        }

        // Reading books using deserialization mechanism
        public bool ReadBooks(string fileName)
        {
            XmlSerializer deserializer = new(typeof(List<Book<TAuthor>>));
            using TextReader textReader = new StreamReader(fileName);
            var data = deserializer.Deserialize(textReader);
            if (data == null)
            {
                return false;
            }
            Books = (List<Book<TAuthor>>)data;
            return true;
        }

        // Saving books using serialization mechanism
        public void WriteBooks(string fileName)
        {
            XmlSerializer serializer = new(typeof(List<Book<TAuthor>>));
            using TextWriter textWriter = new StreamWriter(fileName);
            serializer.Serialize(textWriter, Books);
        }

        // Nested class for comparison books in alphabetical order of names
        class CompareByTitle : IComparer<Book<TAuthor>>
        {
            public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2)
            {
                if (b1 == null || b2 == null)
                {
                    return 0;
                }
                return string.Compare(b1.Title, b2.Title);
            }
        }

        // Nested class for comparison books by the number of authors
        class CompareByAuthorsCount : IComparer<Book<TAuthor>>
        {
            public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2)
            {
                if (b1 == null || b2 == null)
                {
                    return 0;
                }
                return b1.Authors.Count < b2.Authors.Count ? -1 :
                       (b1.Authors.Count == b2.Authors.Count ? 0 : 1);
            }
        }

        // Sort by name
        public void SortByTitle()
        {
            Books.Sort(new CompareByTitle());
        }

        // Sort by count of authors
        public void SortByAuthorsCount()
        {
            Books.Sort(new CompareByAuthorsCount());
        }

        // Overloaded operator of adding book
        public static Bookshelf<TAuthor> operator +(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book)
        {
            Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books };
            newShelf.Add(book);
            return newShelf;
        }

        // Overloaded operator of removal book
        public static Bookshelf<TAuthor> operator -(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book)
        {
            Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books };
            newShelf.Remove(book);
            return newShelf;
        }
    }

    // Titled bookshelf
    public class TitledBookshelf<TAuthor> : Bookshelf<TAuthor>
    {
        public string Title { get; set; }

        // Constructor with parameters
        public TitledBookshelf(string title, params Book<TAuthor>[] books)
                  : base(books)
        {
            Title = title;
        }

        // Definition of string representation
        override public string ToString()
        {
            return Title + "\n" + base.ToString();
        }

        // Overloaded operator of adding book
        public static TitledBookshelf<TAuthor> operator +(TitledBookshelf<TAuthor> titled, Book<TAuthor> book)
        {
            TitledBookshelf<TAuthor> newShelf = new(titled.Title)
            {
                Books = titled.Books
            };
            newShelf.Add(book);
            return newShelf;
        }

        // Overloaded operator of removal book
        public static TitledBookshelf<TAuthor> operator -(TitledBookshelf<TAuthor> titled, Book<TAuthor> book)
        {
            TitledBookshelf<TAuthor> newShelf = new(titled.Title)
            {
                Books = titled.Books
            };
            newShelf.Remove(book);
            return newShelf;
        }
    }
}

Now we can add a new console application (BookshelfApp) to Bookshelf solution. Then we add reference (Add | Project Reference context menu item) to the previously created library. It is necessary to implement testing of all functions. Instead of automatically generated code, we enter the following:

using BookshelfLib;

namespace BookshelfApp
{
    class Program
    {
        static void Main()
        {
            // Create an empty shelf:
            Bookshelf<Author> bookshelf = new();

            // Adding books
            bookshelf += new Book<Author>("The UML User Guide", 1999, 
                                          new Author() { Name = "Grady", Surname = "Booch" },
                                          new Author() { Name = "James", Surname = "Rumbaugh" },
                                          new Author() { Name = "Ivar", Surname = "Jacobson" });
            bookshelf += new Book<Author>("Pro C# 2010 and the .NET 4 Platform", 2010, 
                                          new Author() { Name = "Andrew", Surname =  "Troelsen" });
            bookshelf += new Book<Author>("Thinking in Java", 2005, 
                                          new Author() { Name = "Bruce", Surname = "Eckel" });

            // Display source data:
            Console.WriteLine(bookshelf);
            Console.WriteLine();

            // Looking for books with a certain sequence of characters:
            Console.WriteLine("Enter sequence of characters:");
            string sequence = Console.ReadLine() ?? "";
            Bookshelf<Author> newBookshelf = new()
            {
                Books = bookshelf.ContainsCharacters(sequence)
            };

            // Output to the screen:
            Console.WriteLine("The found books:");
            Console.WriteLine(newBookshelf);
            Console.WriteLine();

            try
            {
                // Save data concerned with books:
                bookshelf.WriteBooks("Bookshelf.xml");

                // Sort by name and save in a file:
                bookshelf.SortByTitle();
                Console.WriteLine("By title:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByTitle.xml");

                // Sort by count of authors and save in a file:
                bookshelf.SortByAuthorsCount();
                Console.WriteLine("By count of authors:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByAuthorsCount.xml");

                // Restore the first shelf in the original version
                bookshelf.ReadBooks("Bookshelf.xml");
                Console.WriteLine("Original state:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();

                // Remove a book about Java
                Book<Author> javaBook = bookshelf[2]; // indexer
                bookshelf -= javaBook;
                Console.WriteLine("After removal of the book:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();

                // Create a new shelf. Use string type to store data
                TitledBookshelf<string> titledBookshelf = new TitledBookshelf<string>("Java");
                titledBookshelf += new Book<string>("Thinking in Java", 2005, "Bruce Eckel");
                Console.WriteLine("Shelf with books on Java language:");
                Console.WriteLine(titledBookshelf);
                titledBookshelf.WriteBooks("JavaBooks.xml");

            }
            catch (Exception ex)
            {
                Console.WriteLine("-----------Exception:-----------");
                Console.WriteLine(ex.GetType());
                Console.WriteLine("------------Message:------------");
                Console.WriteLine(ex.Message);
                Console.WriteLine("----------Stack Trace:----------");
                Console.WriteLine(ex.StackTrace);
            }
        }
    }
}

The new project should be set as the startup project (Set as Startup Project context menu item).

After finishing program, in a subfolder bin\Debug or bin\Release of a project (depending on solution configuration) should appear three XML documents. The first one (Bookshelf.xml) will have such contents:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfBookOfAuthor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <BookOfAuthor Title="The UML User Guide" Year="1999">
        <Authors>
            <Author Surname="Booch" Name="Grady" />
            <Author Surname="Rumbaugh" Name="James" />
            <Author Surname="Jacobson" Name="Ivar" />
        </Authors>
    </BookOfAuthor>
    <BookOfAuthor Title="Pro C# 2010 and the .NET 4 Platform" Year="2010">
        <Authors>
            <Author Surname="Troelsen" Name="Andrew" />
        </Authors>
    </BookOfAuthor>
    <BookOfAuthor Title="Thinking in Java" Year="2005">
        <Authors>
            <Author Surname="Eckel" Name="Bruce" />
        </Authors>
    </BookOfAuthor>
</ArrayOfBookOfAuthor>        

Other XML documents also contain information about books, but in different order.

4 Exercises

  1. Read all lines from a text file to a list, and write these lines into another text file in reverse order.
  2. Read all lines of some text file to a list, and write even-numbered lines to another text file.
  3. Define classes Author and Book. Create objects of these classes. Implement serialization into XML document and deserialization.
  4. Define classes Country and Capital. Create objects of these classes. Implement serialization into XML document and deserialization.
  5. Implement static generic function of copying first element of an array to the last position. Test this function on two arrays of different types.
  6. Implement static generic function of removing array elements with odd indices. Test this function on two arrays of different types.
  7. Implement static generic function of change the order of items on the reverse. Test this function on two lists of different types.
  8. Implement static generic function of swap array element with index 0 and the last element. Test this function on two arrays of different types.
  9. Implement static generic function of swap list item with index 0 and the last element. Test this function on two lists of different types.
  10. Implement static generic function of swap list item with index 1 and second to last item. Test this function on two lists of different types.
  11. Implement static generic function of determining the number times of occurrence a particular item in the list. Test this function on two lists of different types..
  12. Implement static generic function of cyclic shift by specified number of elements. Test this function on two lists of different types.
  13. Implement static generic function of search index of the element from which some list fully included into other one. Test this function on two lists of different types.
  14. Enter elements of a set of real numbers and the range of numbers. Fill this set with random values. Print elements of the set in descending order.
  15. Fill a set of integers with even positive numbers (less or equal to specified number). Print the result.
  16. Enter a word and display all the different letters in alphabetical order.
  17. Enter a sentence and calculate the number of different letters used in the sentence. Ignore spaces and punctuation.
  18. Enter a sentence and calculate the number of different words in a sentence.
  19. Enter a sentence and display all the different words in alphabetical order.
  20. Represent information about users in the form of an associative array (username / password) with the assumption that all user names are different. Display information about users with a password length more than 6 characters.
  21. Create a class "Sentence" with iterator that allows traversal over individual words.
  22. Create a class "Number" with iterator that allows traversal over individual digits.

5 Quiz

  1. What is the purpose of exceptions mechanism?
  2. What is the difference between C# exception handling and C++ exception handling?
  3. How to create an exception object?
  4. Can you use the main result of the function, if an exception throws?
  5. How to get the message concerned with some exception in C#?
  6. How to get stack trace in C#?
  7. How to define a list of exceptions thrown within a function?
  8. Is it possible to call a function that throws an exception, without checking this exception? What will be the reaction of the system?
  9. How to work with text files in .NET applications?
  10. Is it possible to add all of the text file to a single string?
  11. What are the features of the document object model in comparison with an event-oriented model of the document?
  12. What is serialization and why use it?
  13. What are the advantages and disadvantages of XML serialization?
  14. How to set a form of data storage by XML serialization?
  15. What is generic programming?
  16. When should create generic types?
  17. What syntax elements can be generic?
  18. What is the difference between C# generics and C++ templates?
  19. How to determine restrictions on the type parameter?
  20. What container classes are implemented in .NET and what is their purpose?
  21. How to get list from array?
  22. When appropriate to use List versus LinkedList?
  23. When appropriate to use LinkedList versus List?
  24. How do you access the individual elements of the list?
  25. How to sort lists?
  26. What is the difference between sets and lists?
  27. What requirements must meet elements of lists or arrays of objects in order to sort elements without defining criterion of sorting?
  28. What set differs from associative array?
  29. Show examples of the use of associative arrays.
  30. What is the difference between Dictionary and SortedDictionary ?
  31. When you need to create custom containers?
  32. What elements should be implemented when creating your own container?
  33. What is iterator?
  34. What is the purpose of yield keyword?
  35. What are libraries and how they can be used in programming?
  36. What is a dynamic library?
  37. How to connect a class library?
  38. How to create a class library?

 

up