Laboratory Training 4

Generic Programming

1 Training Tasks

1.1 Individual Task

Implement the individual task of the previous laboratory training, replacing the use of arrays with the use of lists (the List class). Instead of working with text files, implement writing and reading through the XML serialization / deserialization mechanism. Reproduce tasks of previous labs.

For all classes that represent entities of the subject area, redefine the methods Equals()and GetHashCode().

Place all classes, except for the class containing the Main() function, in a separate library. Place classes that implement different aspects of functionality in separate files.

In the console application, connect the created library and perform testing of all functions. Implement handling possible exceptions.

1.2 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.3 Creating Custom 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.4 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.5 Working with Dictionary

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

1.6 Use of dynamic Data Type (Advanced Task)

Create a class "A simple fraction". Implement overloading for operations +, -, *, and /, in particular, division by an integer. Create a static class that provides functions for getting arithmetic mean and product of array items. Use dynamic type. Test these functions using both array of doubles and array of simple fractions.

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

2.2 Standard Generic Classes

2.2.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.2.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.2.3 Use of Standard Methods for Working with Lists

Before you create your own generic classes and methods, you should look for standard ones. The List class provides a large number of methods for working with lists, such as Resize() (changing the size keeping existing elements), Reverse() (placing items in reverse order), Sort() (sorting) and many others. The most important methods can be demonstrated in the following example:

List<int> a = new() { 1, 2, 3, 4 };
a.Reverse();
Console.WriteLine(string.Join(" ", a)); // 4 3 2 1
a.Sort();
Console.WriteLine(string.Join(" ", a)); // 1 2 3 4
Console.WriteLine(a.Sum());     // 10
Console.WriteLine(a.Average()); // 2.5
Console.WriteLine(a.Min());     // 1
Console.WriteLine(a.Max());     // 4
a.Remove(3);
Console.WriteLine(string.Join(" ", a)); // 1 2 4
a = a.Concat(new List<int> { 11, 12, 2 }).ToList();
Console.WriteLine(string.Join(" ", a)); // 1 2 4 11 12 2
Console.WriteLine(a.Contains(3));       // False
Console.WriteLine(a.Contains(4));       // True
a = a.Distinct().ToList();
Console.WriteLine(string.Join(" ", a)); // 1 2 4 11 12
Console.WriteLine(a.FindIndex(x => x == 1)); // 0
Console.WriteLine(a.ToString());

2.2.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.2.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.3 Equivalence Checking and Hash Codes

For correct identification and comparison of objects, in particular, for proper operation with Dictionary or HashSet for objects stored in collections, it is advisable to redefine methods Equals() and GetHashCode() of System.Object class.

Checking for equivalence of value types is carried out using the comparison operation ( == ). For some standard classes, such as System.String along with Equals(), the comparison operation is overridden to test strings for equivalence. But in most cases, you should implement the Equals() method to test the equivalence of two objects, because the ordinary comparison is applied to references, not to objects.

The Equals() method should return true if the two objects are equivalent, and false otherwise. A typical implementation includes the following steps:

  • checking references (whether they match);
  • checking the object we are comparing for the value of null;
  • type checking, for example, using GetType();
  • checking field values.

Thanks to the pattern matching mechanism, the implementation Equals() can be simplified. Suppose there is a Person class. The typical implementation of Equals() can be as follows:

public class Person
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";

    public override bool Equals(object? obj)
    {
        return obj is Person person && 
               FirstName == person.FirstName &&
               LastName == person.LastName;
    }
}

If obj equals to null, and also if the object is of a wrong type, the is operator is returns false. Reference checking can be omitted.

Paired with Equals() you usually an overload a method GetHashCode() that returns an integer that represents the object's hash code, a unique value that must be the same for equivalent objects, and preferably different for different objects. The last rule is difficult enough to fulfill. Hash codes are used in hash tables and other data structures to quickly find objects. If the Equals() method is overridden to compare objects by value, it should also be overridden GetHashCode() so that objects with the same values have the same hash code.

In our case, we can offer the following implementation of the GetHashCode() method:

public override int GetHashCode()
{
    return HashCode.Combine(FirstName, LastName);
}

The Combine() method implemented for a different number of arguments (from 1 to 8) allows you to combine hash codes in an optimal way.

2.4 Creating Custom 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.5 dynamic Type

The dynamic contextual keyword was introduced in C# 4.0. Initially, dynamic type was designed to operate with COM objects. The dynamic type allows creation of variables which types can be determined at runtime, unlike var keyword, which allows type identification at compile time. Therefore, variable can change its actual type:

dynamic x = 1;
Console.WriteLine(x.GetType().Name); // Int32
x = "A";
Console.WriteLine(x.GetType().Name); // String
x = 2.5;
Console.WriteLine(x.GetType().Name); // Double

Note: the GetType() method returns an object of System.Type class that encapsulates information about variable's type, in particular its name (the Name property).

The following example shows the implementation of generic paradigm based on the dynamic type. The Sum() method can be applied to arguments of different types (int, double, string) because these types allow application of + operator:

class Program
{
    static dynamic Sum(dynamic x, dynamic y)
    {
        return x + y;
    }

    static void Main(string[] args)
    {
        int i1 = 1;
        int i2 = 2;
        Console.WriteLine(Sum(i1, i2)); // 3
        double d1 = 1.1; 
        double d2 = 2.2;
        Console.WriteLine(Sum(d1, d2)); // 3.3
        string s1 = "dot";
        string s2 = "net";
        Console.WriteLine(Sum(s1, s2)); // dotnet
    }
}

We also can create class (e.g. Complex) that overloads + operator. Objects of this class also can be used as actual arguments:

public class Complex
{
    public double A { get; set; }
    public double B { get; set; }
    public static Complex operator+(Complex c1, Complex c2)
    {
        return new Complex { A = c1.A + c2.A, B = c1.B + c2.B };
    }
    public override string ToString()
    {
        return A + " " + B + "i";
    }
}

. . .

Complex c1 = new Complex { A = 1, B = 2 };
Complex c2 = new Complex { A = 3, B = 4 };
Console.WriteLine(Sum(c1, c2));

We can try to send arguments of types, which do not support + operator. In this case compiler does not show error messages:

object o1 = new object();
object o2 = new object();
Console.WriteLine(Sum(o1, o2));

But at runtime we'll get an exception:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 
Operator '+' cannot be applied to operands of type 'object' and 'object'

The disadvantages of dynamic types are some decreasing of program effectiveness and extra debug difficulties. Possible errors can be found at runtime, but not at compile time.

You can create array of dynamic items. Actually, items can be of different types:

dynamic[] arr = new dynamic[3];
arr[0] = 1;
arr[1] = 1.2;
arr[2] = "a";
//arr[2] = new object();
dynamic sum = 0;
foreach (dynamic d in arr)
{
    sum += d;
}
Console.WriteLine(sum); // 2.2a

2.6 Use of Attributes

C# attributes are used to add metadata to code elements such as classes, methods, and properties. Attributes allow you to add information to a code element that cannot be defined by C# syntax. Attribute processing is done at compile time. The attribute is placed in square brackets in front of the code element to which this attribute refers. There are several standard attributes that can always be used in the program code.

An example of a standard attribute is [Obsolete], which marks obsolete methods, the use of which is undesirable. After the name of the attribute in parentheses a parameter is put: a string that informs that this method is obsolete and what exactly is recommended to be used instead of this method:

class Program
{
    [Obsolete("This method is deprecated, use Good instead.")]
    static void Bad()
    {
    }

    static void Good()
    {

    }

    static void Main(string[] args)
    {
        Bad(); // warning: deprecated method call
    }
}

You can completely prohibit the use of obsolete methods by setting true as the second parameter, which causes an error:

class Program
{
    [Obsolete("This method is deprecated, use Good instead.", true)]
    static void Bad()
    {
    }

    static void Good()
    {

    }

    static void Main(string[] args)
    {
        Bad(); // syntax error
    }
}

You can also use attributes like [Serializable], [DefaultValue], [MaxLength], [MinLength] etc.

To create your own attribute, you need to describe a class derived from System.Attribute. A class can contain fields, properties, methods, etc. Parameters of class constructors are specified when applying attributes in the code.

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

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

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 Custom 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 LabFourthArray
{
    // 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 Calculation of sums of array items of different types

Suppose we need to create a function to calculate the sum of array items for arrays of different types. We can create such a function using the type dynamic. The program will be as follows:

namespace DynamicSum
{
    class Program
    {
        public static dynamic Sum(dynamic arr)
        {
            dynamic result = arr[0];
            for (int i = 1; i < arr.Length; i++)
            {
                result += arr[i];
            }
            return result;
        }

        static void Main(string[] args)
        {
            int[] ints = { 1, 2, 3 };
            Console.WriteLine(Sum(ints));
            string[] strings = { "a", "b", "c" };
            Console.WriteLine(Sum(strings));
        }
    }
}

The result of the program will be as follows:

6
abc

As can be seen from the given code, the sum can be found for arrays of all objects for which the + operator is defined.

3.6 Working with the List of Publications and XML Serialization

Suppose we decided to use lists instead of arrays in the program for processing data about publications on a bookshelf, which was considered in previous labs. In addition, it is advisable to extend the program with functions for saving data in XML format and loading data from an XML document. Work with XML documents will be based on serialization mechanisms.

Since multi-use of software expected, all classes that describe entities of our domain should be placed in a separate library. This allows us to 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 (BookshelfApp). 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 Publications.cs. This should be done using Properties window, selecting Class1.cs file in the Solution Explorer.

For more convenient work with the library, separate groups of classes related to various functions (representation of entities, obtaining a view in the form of lines, searching and sorting, working with a text file) can be placed in separate files.

In the new version of the program, authors in books and publications on the shelf will be stored in lists instead of arrays. This simplifies some actions, for example, related to adding and removing elements. We add overloaded methods to all classes. Taking into account further XML-serialization, it is necessary to add some attributes to the previously designed software entities, in particular, before the properties for using XML-attributes instead of individual tags. The code of the file Publications.cs will be as follows:

// Publications.cs
using System.Xml.Serialization;

namespace BookshelfLib
{
    /// <summary>
    /// Represents the author of the book on the bookshelf
    /// </summary>
    public class Author
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Name { get; set; } = "";

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

        public Author() { }
        public Author(string name, string surname)
        {
            Name = name;
            Surname = surname;
        }

        //// <summary>
        /// Provides a string representation of author data
        /// </summary>
        /// <returns>a string representing the author of the book</returns>
        public override string ToString()
        {
            return StringRepresentations.ToString(this);
        }

        /// <summary>
        /// Determines whether the specified object is equal to the current object
        /// </summary>
        /// <param name="obj">The object to compare with the current object</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        public override bool Equals(object? obj)
        {
            return obj is Author author &&
                   Name == author.Name &&
                   Surname == author.Surname;
        }

        /// <summary>
        /// Generates the hash code of the object
        /// </summary>
        /// <returns>object hash code</returns>
        public override int GetHashCode()
        {
            return HashCode.Combine(Name, Surname);
        }
    }

    /// <summary>
    /// Represents a separate publication (book, magazine, etc.)
    /// </summary>
    [XmlInclude(typeof(Book))]
    [XmlInclude(typeof(Magazine))]
    public abstract class Publication
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Title { get; set; } = "";

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

        /// <summary>
        /// Converts publication data into a line of a text file
        /// </summary>
        /// <returns>a string ready to be written to a text file</returns>
        abstract public string ToFileData();

        /// <summary>
        /// Creates an object whose data is read from a line of a text file
        /// </summary>
        /// <param name="data">string with data about publication read from text file</param>
        /// <returns>the object whose data is read from the string</returns>
        abstract public Publication FromFileData(string data);

        /// <summary>
        /// Determines whether the specified object is equal to the current object
        /// </summary>
        /// <param name="obj">The object to compare with the current object</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        public override bool Equals(object? obj)
        {
            return obj is Publication publication &&
                   Title == publication.Title &&
                   Year == publication.Year;
        }

        /// <summary>
        /// Generates the hash code of the object
        /// </summary>
        /// <returns>object hash code</returns>
        public override int GetHashCode()
        {
            return HashCode.Combine(Title, Year);
        }
    }

    /// <summary>
    /// Represents a book on a bookshelf
    /// </summary>
    public class Book : Publication
    {
        public Author[] Authors { get; set; } = { };
        
        public Book() { }
        public Book(string title, int year)
        {
            Title = title;
            Year = year;
        }
        
        /// <summary>
        /// Provides a string representation of the book data
        /// </summary>
        /// <returns>a string representing data about the book</returns>
        public override string ToString()
        {
            return StringRepresentations.ToString(this);
        }

        /// <summary>
        /// Converts book data into a line of a text file
        /// </summary>
        /// <returns>a string ready to be written to a text file</returns>
        public override string ToFileData()
        {
            return FileData.ToFileData(this);
        }

        /// <summary>
        /// Creates an object whose data is read from a line of a text file
        /// </summary>
        /// <param name="data">string with data about publication read from text file</param>
        /// <returns>the object whose data is read from the string</returns>
        public override Publication FromFileData(string data)
        {
            return FileData.BookFromFileData(data);
        }
        
        /// <summary>
        /// Creates and adds an author to the authors array
        /// </summary>
        /// <param name="name">author's name</param>
        /// <param name="surname">author's surname</param>public void AddAuthor(string name, string surname)
        {
            Authors.Add(new Author(name, surname));
        }

        /// <summary>
        /// Removes data about the author
        /// </summary>
        /// <param name="author">author whose data should be found and deleted</param>
        public void RemoveAuthor(string name, string surname)
        {
            Authors.RemoveAll(author => author.Name == name && author.Surname == surname);
        }

        /// <summary>
        /// Determines whether the specified object is equal to the current object
        /// </summary>
        /// <param name="obj">The object to compare with the current object</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        public override bool Equals(object? obj)
        {
            return obj is Book book &&
                   base.Equals(book) &&
                   Enumerable.SequenceEqual(Authors, book.Authors);
        }

        /// <summary>
        /// Generates the hash code of the object
        /// </summary>
        /// <returns>object hash code</returns>
        public override int GetHashCode()
        {
            return HashCode.Combine(Title, Year, Authors);
        }
    }

    /// <summary>
    /// Represents a magazine on the shelf
    /// </summary>
    public class Magazine : Publication
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public int Volume { get; set; }

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

        /// <summary>
        /// Provides a string representation of a magazine
        /// </summary>
        /// <returns>a string representing a magazine</returns>
        public override string ToString()
        {
            return StringRepresentations.ToString(this);
        }

        /// <summary>
        /// Converts a magazine data into a line of a text file
        /// </summary>
        /// <returns>a string ready to be written to a text file</returns>
        public override string ToFileData()
        {
            return FileData.ToFileData(this);
        }

        /// <summary>
        /// Creates an object whose data is read from a line of a text file
        /// </summary>
        /// <param name="data">publication data string read from text file</param>
        /// <returns>the object whose data is read from the string</returns>
        public override Publication FromFileData(string data)
        {
            return FileData.MagazineFromFileData(data);
        }

        /// <summary>
        /// Determines whether the specified object is equal to the current object
        /// </summary>
        /// <param name="obj">The object to compare with the current object</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        public override bool Equals(object? obj)
        {
            return obj is Magazine magazine &&
                   base.Equals(magazine) &&
                   Volume == magazine.Volume &&
                   Number == magazine.Number;
        }

        /// <summary>
        /// Generates the hash code of the object
        /// </summary>
        /// <returns>object hash code</returns>
        public override int GetHashCode()
        {
            return HashCode.Combine(Title, Year, Volume, Number);
        }
    }

    /// <summary>
    /// Bookshelf
    /// </summary>
    public class Bookshelf
    {
        /// <summary>
        /// List of references to publications
        /// </summary>
        public List<Publication> Publications { get; set; } = new();

        /// <summary>
        /// An indexer that allows getting a publication by index
        /// </summary>
        /// <param name="index">publication index</param>
        /// <returns>publication with corresponding index</returns>
        public Publication this[int index]
        {
            get => Publications[index];
            set => Publications[index] = value;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        public Bookshelf() { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="publications">open array of publications</param>
        public Bookshelf(params Publication[] publications)
        {
            Publications.AddRange(publications);
        }

        /// <summary>
        /// Adds a new publication to the bookshelf
        /// </summary>
        /// <param name="publication">the publication to add to the bookshelf</param>
        public void AddPublication(Publication publication)
        {
            Publications.Add(publication);
        }

        /// <summary>
        /// Deletes the publication with the specified title
        /// </summary>
        /// <param name="title">the title of the publication to find and delete</param>
        public void Remove(string title)
        {
            Publications.RemoveAll(publication => publication.Title == title);
        }

        /// <summary>
        /// Provides a string representation of the bookshelf data
        /// </summary>
        /// <returns>a string representing the bookshelf data</returns>
        public override string ToString()
        {
            return StringRepresentations.ToString(this);
        }

        /// Determines whether the specified object is equal to the current object
        /// </summary>
        /// <param name="obj">The object to compare with the current object</param>
        /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
        public override bool Equals(object? obj)
        {
            return obj is Bookshelf bookshelf &&
                   Enumerable.SequenceEqual(Publications, bookshelf.Publications);
        }

        /// <summary>
        /// Generates the hash code of the object
        /// </summary>
        /// <returns>object hash code</returns>
        public override int GetHashCode()
        {
            return Publications.GetHashCode();
        }
    }
}

As can be seen from the given code, the possible derived types of the Publication class are listed in individual attributes. Unfortunately, such a list is a violation of the Dependency Inversion Principle, but it is a requirement of the XML serialization technology.

In the ToString.cs file, we will place a StringRepresentations class with methods for obtaining a representation of various entities with strings:

// ToString.cs
using System.Text;

namespace BookshelfLib
{
    /// <summary>
    /// A static class that provides a string representation
    /// of various application objects
    /// </summary>
    public static class StringRepresentations
    {
        /// <summary>
        /// Provides a string representation of author data
        /// </summary>
        /// <param name="author">author of the book</param>
        /// <returns>a string representing the author of the book</returns>
        public static string ToString(Author author)
        {
            return author.Name + " " + author.Surname;
        }

        /// <summary>
        /// Provides a string representation of the book data
        /// </summary>
        /// <param name="book">book</param>
        /// <returns>a string representing data about the book</returns>
        public static string ToString(Book book)
        {
            if (book == null)
            {
                return "";
            }
            string result = string.Format("Book. Title: \"{0}\". Year of publication: {1}", 
                                          book.Title, book.Year);
            result += "   Authors:\n";
            for (int i = 0; i < book.Authors.Count; i++)
            {
                result += string.Format("      {0}", book.Authors[i]);
                result += (i < book.Authors.Count - 1 ? "," : "") + "\n";
            }
            return result;
        }

        /// <summary>
        /// Provides a string representation of a magazine
        /// </summary>
        /// <param name="magazine">magazine</param>
        /// <returns>a string representing a magazine</returns>
        public static string ToString(Magazine magazine)
        {
            if (magazine == null)
            {
                return "";
            }
            return string.Format("Magazine. Title: \"{0}\". Year of publication: {1}. Volume: {2}. Number: {3}",
                                  magazine.Title, magazine.Year, magazine.Volume, magazine.Number);
        }

        /// <summary>
        /// Provides a string representation of the bookshelf data
        /// </summary>
        /// <param name="bookshelf">bookshelf</param>
        /// <returns>a string representing the bookshelf data</returns>
        public static string ToString(Bookshelf bookshelf)
        {
            StringBuilder result = new ("");
            foreach (Publication publication in bookshelf.Publications)
            {
                result.Append(publication + "\n");
            }
            return result.ToString();
        }
    }
}

The file BookshelfProcessor.cs will contain a BookshelfProcessor class with methods that perform searching and sorting:

// BookshelfProcessor.cs
namespace BookshelfLib
{
    /// <summary>
    /// Provides methods for finding and sorting publications on a shelf
    /// </summary>
    public class BookshelfProcessor
    {

        /// <summary>
        /// Searches for a specified sequence of characters in publication titles
        /// </summary>
        /// <param name="bookshelf">bookshelf</param>
        /// <param name="characters">character sequence to find</param>
        /// <returns>an array of publications whose titles contain the specified sequence</returns>
        public static List<Publication> ContainsCharacters(Bookshelf bookshelf, string characters)
        {
            return bookshelf.Publications.FindAll(publication => publication.Title.Contains(characters));
        }

        /// <summary>
        /// Sorts publications alphabetically by name ignoring case
        /// </summary>
        /// <param name="bookshelf">bookshelf</param>
        public static void SortByTitles(Bookshelf bookshelf)
        {
            bookshelf.Publications.Sort((b1, b2) => string.Compare(b1.Title.ToUpper(), b2.Title.ToUpper()));
        }
    }
}

We will place the previously created classes that provide reading from text files and writing to text files in the file TxtFiles.cs:

// TxtFiles.cs
using System.Text;

namespace BookshelfLib
{

    /// <summary>
    /// Provides methods for converting object data to lines of a text file and vice versa
    /// </summary>
    public static class FileData
    {
        /// <summary>
        /// Converts book data into a line of a text file
        /// </summary>
        /// <param name="book">the book whose data is converted to string</param>
        /// <returns>a string ready to be written to a text file</returns>
        public static string ToFileData(Book book)
        {
            string s = string.Format("{0}\t{1}\t{2}", book.GetType().ToString(), book.Title, book.Year);
            StringBuilder sb = new(s);
            foreach (Author author in book.Authors)
            {
                sb.Append("\t" + author);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Converts a magazine data into a line of a text file
        /// </summary>
        /// <param name="magazine">the magazine whose data is converted to a string</param>
        /// <returns>a string ready to be written to a text file</returns>
        public static string ToFileData(Magazine magazine)
        {
            return string.Format("{0}\t{1}\t{2}\t{3}\t{4}", magazine.GetType().ToString(), magazine.Title,
                magazine.Year, magazine.Volume, magazine.Number);
        }

        /// <summary>
        /// Creates an object whose data is read from a line of a text file
        /// </summary>
        /// <param name="data">string with book data read from text file</param>
        /// 
        /// <returns>the object whose data is read from the string</returns>
        public static Book BookFromFileData(string data)
        {
            string[] parts = data.Split('\t');
            Book book = new(title: parts[1], year: int.Parse(parts[2]));
            foreach (string author in parts[3..^0])
            {
                string[] authorData = author.Split(' ');
                book.AddAuthor(name: authorData[0], surname: authorData[1]);
            }
            return book;
        }

        /// <summary>
        /// Creates an object whose data is read from a line of a text file
        /// </summary>
        /// <param name="data">string with magazine data read from text file</param>
        /// 
        /// <returns>the object whose data is read from the string</returns>
        public static Magazine MagazineFromFileData(string data)
        {
            string[] parts = data.Split('\t');
            Magazine magazine = new()
            {
                Title = parts[1],
                Year = int.Parse(parts[2]),
                Volume = int.Parse(parts[3]),
                Number = int.Parse(parts[4])
            };
            return magazine;
        }
    }

    /// <summary>
    /// Provides methods for reading from file and writing to a file
    /// </summary>
    public static class FileUtils
    {
        /// <summary>
        /// Writes data about the bookshelf to a text file
        /// </summary>
        /// <param name="bookshelf">reference to bookshelf</param>
        /// <param name="fileName">file name</param>
        public static void WriteToFile(Bookshelf bookshelf, string fileName)
        {
            using StreamWriter writer = new(fileName);
            foreach (Publication publication in bookshelf.Publications)
            {
                writer.WriteLine(publication.ToFileData());
            }
        }

        /// <summary>
        /// Reads data about the bookshelf from a text file
        /// </summary>
        /// <param name="fileName">file name</param>
        /// <returns>the bookshelf object</returns>
        public static Bookshelf ReadFromFile(string fileName)
        {
            System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            Bookshelf bookshelf = new();
            using StreamReader reader = new(fileName);
            string? s;
            while ((s = reader.ReadLine()) != null)
            {
                string typeName = s.Split()[0];
                if (assembly.CreateInstance(typeName) is Publication publication)
                {
                    bookshelf.AddPublication(publication.FromFileData(s));
                }
            }
            return bookshelf;
        }
    }
}

We create a separate file (XMLHandle.cs) for the XMLHandle class. The methods of this class will provide serialization and deserialization of the bookshelf data:

// XMLHandle.cs
using System.Xml.Serialization;

namespace BookshelfLib
{
    /// <summary>
    /// Provides methods for reading from and writing to an XML document
    /// </summary>
    public static class XMLHandle
    {
        /// <summary>
        /// Reads data about publications using the serialization mechanism
        /// </summary>
        /// <param name="fileName">file name</param>
        /// <returns>a reference to a new bookshelf</returns>
        public static Bookshelf ReadFromFile(string fileName)
        {
            XmlSerializer deserializer = new(typeof(Bookshelf));
            using TextReader textReader = new StreamReader(fileName);
            var data = deserializer.Deserialize(textReader);
            if (data == null)
            {
                return new();
            }
            Bookshelf bookshelf = data as Bookshelf ?? new ();
            return bookshelf;
        }

        /// <summary>
        /// Writes publications using the serialization mechanism
        /// </summary>
        /// <param name="bookshelf">a reference to the bookshelf</param>
        /// <param name="fileName">file name</param>
        public static void WriteToFile(Bookshelf bookshelf, string fileName)
        {
            XmlSerializer serializer = new(typeof(Bookshelf));
            using TextWriter textWriter = new StreamWriter(fileName);
            serializer.Serialize(textWriter, bookshelf);
        }
    }
}

Now we add a reference (Add | Project Reference) to the previously created library to the console application (project BookshelfApp) through the context menu. All functions should be checked. The code of the Program.cs file will be as follows:

// Program.cs
namespace BookshelfApp;

using BookshelfLib;

/// <summary>
/// A console application to demonstrate working with publications on a bookshelf
/// </summary>
class Program
{
    /// <summary>
    /// Prepares test data to demonstrate working with publications on a bookshelf
    /// </summary>
    /// <returns>Bookshelf with added publications</returns>
    public static Bookshelf CreateBookshelf()
    {
        return new Bookshelf(
            new Book(@"The UML User Guide", 1999)
            {
                Authors = new() {
                    new Author("Grady", "Booch"),
                    new Author("James", "Rumbaugh"),
                    new Author("Ivar", "Jacobson")
                }
            },
            new Book(@"Pro C# 2010 and the .NET 4 Platform", 2010)
            {
                Authors = new() { new Author("Andrew", "Troelsen") }
            },
            new Book(@"Thinking in Java", 2005)
            {
                Authors = new() { new Author("Bruce", "Eckel") }
            },
            new Book(@"Design Patterns: Elements of Reusable Object-Oriented Software", 1994)
            {
                Authors = new() {
                    new Author("Erich", "Gamma"),
                    new Author("Richard", "Helm"),
                    new Author("Ralph", "Johnson"),
                    new Author("John", "Vlissides")
                }
            },
            new Book(@"C# 9.0 in a Nutshell: The Definitive Reference", 2021)
            {
                Authors = new() { new Author("Joseph", "Albahari") }
            },
            new Magazine()
            {
                Title = @"The Journal of Object Technology", 
                Year = 2024,
                Volume = 23,
                Number = 3 
            }
        );
    }

    /// <summary>
    /// Demonstrates how to search and sort publications
    /// </summary>
    /// <param name="bookshelf">the bookshelf for which the work is demonstrated</param>
    public static void HandleBookshelf(Bookshelf bookshelf)
    {
        Console.WriteLine("\nInitial state:");
        Console.WriteLine(bookshelf);
        Console.WriteLine("\nTitles that contain \"The\"");
        var result = BookshelfProcessor.ContainsCharacters(bookshelf, "The");
        foreach (var publication in result) 
        {
            Console.WriteLine(publication.Title);
        }
        //Console.WriteLine(result.ToArray().Length > 0 ? string.Join("\n", result) : "No");
        Console.WriteLine("\nAlphabetically ignoring case:");
        BookshelfProcessor.SortByTitles(bookshelf);
        Console.WriteLine(bookshelf);
    }

    /// <summary>
    /// Outputs the exception information
    /// </summary>
    /// <param name="ex">the exception for which data is output</param>
    internal static void ShowException(Exception ex)
    {
        Console.WriteLine("-----------Exception:-----------");
        Console.WriteLine(ex.GetType());
        Console.WriteLine("------------Content:------------");
        Console.WriteLine(ex.Message);
        Console.WriteLine("----------Stack Trace:----------");
        Console.WriteLine(ex.StackTrace);
    }

    /// <summary>
    /// Demonstrates working with a file, as well as methods 
    /// for adding and removing authors and publications
    /// </summary>
    /// <param name="fileName">file name</param>
    public static void AdditionalProcessing(string fileName)
    {
        try
        {
            Console.WriteLine("("Reading from the file :" + fileName);
            Bookshelf bookshelf = FileUtils.ReadFromFile(fileName);
            Console.WriteLine(bookshelf);
            Console.WriteLine("Adding and removing the author:");
            if (bookshelf[0] is Book book)
            {
                book.AddAuthor("Elon", "Musk");
                Console.WriteLine(bookshelf[0]);
                book.RemoveAuthor("Elon", "Musk");
                Console.WriteLine(bookshelf[0]);
            }
            Console.WriteLine("Remove Java book	");
            bookshelf.Remove("Thinking in Java");
            Console.WriteLine(bookshelf);
        }
        catch (IOException ex)
        {
            Console.WriteLine("Error reading from file " + fileName);
            ShowException(ex);
        }
    }

    /// <summary>
    /// Demonstrates working with an XML document
    /// </summary>
    /// <param name="fileName">file name</param>
    /// <returns></returns>
    public static void XMLProcessing(Bookshelf bookshelf, string fileName)
    {
        try
        {
            XMLHandle.WriteToFile(bookshelf, "publications.xml");
            Console.WriteLine("Reading from an XML file :" + fileName);
            bookshelf = XMLHandle.ReadFromFile(fileName);
            Console.WriteLine(bookshelf);
        }
        catch (IOException ex)
        {
            Console.WriteLine("Error working with file " + fileName);
            ShowException(ex);
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine("Error working with XML document");
            ShowException(ex);
        }
    }

    /// <summary>
    /// The starting point of the console application
    /// </summary>
    static void Main()
    {
        Console.OutputEncoding = System.Text.Encoding.UTF8;
        Bookshelf bookshelf = CreateBookshelf();
        HandleBookshelf(bookshelf);
        FileUtils.WriteToFile(bookshelf, "publications.txt");
        AdditionalProcessing("books.txt"); // No such file
        AdditionalProcessing("publications.txt");
        XMLProcessing(bookshelf, "publications.xml");
    }
}

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

After the program is finished, an XML document publications.xml will appear in the bin\Debug or bin\Release folder of the project (depending on the way the program was loaded for execution), which will have the following content:

<?xml version="1.0" encoding="utf-8"?>
<Bookshelf xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Publications>
    <Publication xsi:type="Book" Title="C# 9.0 in a Nutshell: The Definitive Reference" Year="2021">
      <Authors>
        <Author Name="Joseph" Surname="Albahari" />
      </Authors>
    </Publication>
    <Publication xsi:type="Book" Title="Design Patterns: Elements of Reusable Object-Oriented Software" Year="1994">
      <Authors>
        <Author Name="Erich" Surname="Gamma" />
        <Author Name="Richard" Surname="Helm" />
        <Author Name="Ralph" Surname="Johnson" />
        <Author Name="John" Surname="Vlissides" />
      </Authors>
    </Publication>
    <Publication xsi:type="Book" Title="Pro C# 2010 and the .NET 4 Platform" Year="2010">
      <Authors>
        <Author Name="Andrew" Surname="Troelsen" />
      </Authors>
    </Publication>
    <Publication xsi:type="Magazine" Title="The Journal of Object Technology" Year="2024" Volume="23" Number="3" />
    <Publication xsi:type="Book" Title="The UML User Guide" Year="1999">
      <Authors>
        <Author Name="Grady" Surname="Booch" />
        <Author Name="James" Surname="Rumbaugh" />
        <Author Name="Ivar" Surname="Jacobson" />
      </Authors>
    </Publication>
    <Publication xsi:type="Book" Title="Thinking in Java" Year="2005">
      <Authors>
        <Author Name="Bruce" Surname="Eckel" />
      </Authors>
    </Publication>
  </Publications>
</Bookshelf>

4 Exercises

  1. Define classes Student and Academic group (with the field – an array of students). Create objects of these classes. Implement serialization into XML document and deserialization.
  2. Define classes Author and Book. Create objects of these classes. Implement serialization into XML document and deserialization.
  3. Define classes Country and Capital. Create objects of these classes. Implement serialization into XML document and deserialization.
  4. Implement static generic function of copying first element of an array to the last position. Test this function on two arrays of different types.
  5. Implement static generic function of removing array elements with odd indices. Test this function on two arrays of different types.
  6. Implement static generic function of change the order of items on the reverse. Test this function on two lists of different types.
  7. Implement static generic function of swap array element with index 0 and the last element. Test this function on two arrays of different types.
  8. Implement static generic function of swap list item with index 0 and the last element. Test this function on two lists of different types.
  9. 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.
  10. 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.
  11. Implement static generic function of cyclic shift by specified number of elements. Test this function on two lists of different types.
  12. 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.
  13. 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.
  14. Fill a set of integers with even positive numbers (less or equal to specified number). Print the result.
  15. Enter a word and display all the different letters in alphabetical order.
  16. Enter a sentence and calculate the number of different letters used in the sentence. Ignore spaces and punctuation.
  17. Enter a sentence and calculate the number of different words in a sentence.
  18. Enter a sentence and display all the different words in alphabetical order.
  19. 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.
  20. Create a class "Sentence" with iterator that allows traversal over individual words.
  21. Create a class "Number" with iterator that allows traversal over individual digits.

5 Quiz

  1. What is generic programming?
  2. When should create generic types?
  3. What syntax elements can be generic?
  4. What is the difference between C# generics and C++ templates?
  5. How to determine restrictions on the type parameter?
  6. What container classes are implemented in .NET and what is their purpose?
  7. How to get list from array?
  8. When appropriate to use List versus LinkedList?
  9. When appropriate to use LinkedList versus List?
  10. How do you access the individual elements of the list?
  11. How to sort lists?
  12. What is the difference between sets and lists?
  13. What requirements must meet elements of lists or arrays of objects in order to sort elements without defining criterion of sorting?
  14. What set differs from associative array?
  15. Show examples of the use of associative arrays.
  16. What is the difference between Dictionary and SortedDictionary ?
  17. When you need to create custom containers?
  18. What elements should be implemented when creating your own container?
  19. What is iterator?
  20. What is the purpose of yield keyword?
  21. What's difference between var and dynamic definitions?
  22. How to implement generic approach using dynamic typing?
  23. What are the features of the document object model in comparison with an event-oriented model of the document?
  24. What is serialization and why use it?
  25. What are the advantages and disadvantages of XML serialization?
  26. How to set a form of data storage by XML serialization?
  27. What are libraries and how they can be used in programming?
  28. What is a dynamic library?
  29. How to connect a class library?
  30. How to create a class library?

 

up