Laboratory Training 2

Inheritance and Polymorphism

1 Training Tasks

1.1 Individual task

Expand the previous program with the hierarchy of entities according to the following table:

Base Class Derived Class
Group of people Academic group
Institution Institute
Structural unit Faculty
Form of knowledge control Session
Settlement Town
Territory Administrative region
Association Sports section
Sports Club Football team
Creative team Musical band
Creative team Musical band
Collection of music Album
Habitation Flat
Book Storybook
Artist Painter
Artist Writer

Reproduce task of prior laboratory training. Overload the following operators:

  • addition operator (+) for adding objects to the group
  • subtraction operator (-) removing objects from the group.

Override ToString() and Equals() methods for all classes. In order to represent additional data create an appropriate structure (instead of string).

1.2 Expansion of String Class

Extend the standard System.String class with method of removing extra spaces (multiple spaces should be replaced with single space character).

1.3 Creation of Class "Complex"

Create a class Complex (complex number), overload the following operations: +, -, *, / , and implicit cast to string type. Use object initializers for creation test objects.

1.4 Creation of Class "Vector"

Create a class Vector (mathematical vector). Define necessary constructors. Overload the following operators:

  • + (sum of vectors)
  • - (difference vectors)
  • * (multiplication by a scalar value)
  • * (scalar product)
  • / (division by a scalar value)

Overload ToString() method. The Main() function create three objects of type Vector and implement testing of created operations.

1.5 Roots of an Equation

Implement a program that allows you to find all the roots of a certain equation on a given interval. Algorithm for finding roots is a sequential scan on the interval with a certain step. If the function changes sign on particular subrange, you should print the arithmetic mean of subinterval beginning and end.

Implement two approaches: Implement two approaches: using abstract classes and with interfaces.

1.6 3D Point

Implement structure (struct) to represent a point in three-dimensional space. Implement a function calculating the distance from the point to the origin. Test structure in a separate class.

2 Instructions

2.1 Object Initializers

Traditionally languages of object-oriented programming provide constructors with parameters to initialize fields with necessary values. However, sometimes set of constructors do not meet our requirements, so we need to set values of some properties manually. Using C# 3.0 (and later), you can initialize public properties and public fields in a special block placed after constructor invocation. Expressions of initialization are listed separated by commas.

Assume the following class:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

There are two syntax forms of initializers:

Point p = new Point
{
    X = 10,
    Y = 20
};
Point p1 = new Point()
{
    X = 100,
    Y = 200
};    

In these examples, it first calls the constructor with no parameters, and then the specified values are put into properties. You can also call the constructor with parameters.

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public Point() { }
    public Point(int x, int y)
    {
        X = x; 
        Y = y;
    }
}

...

Point p = new Point(1, 2)
{
    X = 10,
    Y = 20
};

Since the initialization block is executed after constructor, the values assigned in the constructor will be overwritten by the values specified in the block. More useful is another example in which the property is initialized to the appropriate Color type defined in the namespace System.Drawing. Coordinates of the point can be defined using constructor. The value of color is set using initializer because of lack of appropriate constructor.

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public System.Drawing.Color Color { get; set; } 
    public Point() { }
    public Point(int x, int y)
    {
        X = x; 
        Y = y;
    }
}

...

Point p = new Point(1, 2)
{ 
    Color = System.Drawing.Color.Blue
};    

Sometimes there is a need for nested initializers. Suppose class Rectangle defines properties of Point type:

public class Rectangle
{
    public Point LeftTop { get; set; }
    public Point RightBottom { get; set; }
}

You can call constructors from initialization block:

Rectangle rect = new Rectangle 
{ 
    LeftTop = new Point(0, 0), 
    RightBottom = new Point(30, 40) 
};    

You can also use the following nested initialization:

Rectangle rect = new Rectangle
{
    LeftTop = new Point 
    {
        X = 0, 
        Y = 0
    },
    RightBottom = new Point
    {
        X = 30,
        Y = 40
    }
};    

2.2 Static Classes

The version 2.0 of C# language introduces concept of static classes. Static classes can define static members only.

public static class Static
{
    private static int k = 144;

    public static int getK()
    {
        return k;
    }
}    

Static classes cannot be used for creation of objects, as well as creation of derived classes. These classes are sealed by default. Static class can have a static constructor.

2.3 Operator Overloading

C# supports overloading for some operators:

  • Unary operators: +, -, !, ~, ++, --, true, false
  • Binary operators: +, -, *, /, %, &, |, ^, <<, >>, ==, !=, <, >, <=, >=

Overloading of true and false operators allows use references to object in conditional operation, if statements, and cycle headers. Some operators, namely true and false, == and !=, > and <, >= and <=, are pairs: if you decide to overload one of pair operators you must overload also second operator of this pair.

To overload operator, you should create public static method which name is constructed from operator keyword and corresponding operator.

The explicit operator Type_name() and implicit operator Type_name() are used for explicit and implicit type conversion. Implicit conversions in C# correspond to special conversion operators (conversion from a class to another type) and to constructors with one parameter (conversion from another type to this class).

The following class provides overloading of "plus" operator and implicit conversion to string type:

using System;

namespace PointTest
{
    class Point
    {
        private double x, y;

        public Point(double x, double y) 
        {
            this.x = x; 
            this.y = y;
        }

        public static Point operator+(Point a, Point b) 
        { 
            return new Point(a.x + b.x, a.y + b.y);
        }
    
        public static implicit operator string(Point p)
        {
            return p.x + " " + p.y;
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            Point p1 = new Point(1, 2);
            Point p2 = new Point(3, 4);
            Point p3 = p1 + p2;
            Console.WriteLine(p3); // cast to string
        }
    }
}

If you overload some arithmetic operator (e.g. "+"), appropriate self-assigned operator is automatically overloaded (e.g. "+=").

2.4 Inheritance

Inheritance is the creation of derived classes from base classes. Objects of derived class implicitly contain all the fields of the base class, including private, despite the fact that the methods of the derived class cannot access private members of the base class. In addition, all public properties and methods are inherited. The base class members with protected modifier are available from derived classes.

Unlike C++, C# allows only single inheritance for classes. In contrast to C++, inheritance is always public. To create derived class, you should use colon character followed by base class name:

class DerivedClass : BaseClass 
{
  // class body
}

All .NET classes are implicitly or explicitly derived from System.Object.

Constructors cannot be inherited. Each class of hierarchy should implement its own set of constructors. The base class constructor is automatically called each time you invoke constructor of a derived class.

The base keyword is used for access to base class elements from the derived class, especially:

  • for invocation of overridden method of a base class
  • for transferring arguments to base class constructor

For example:

class BaseClass
{
    int i, j;

public BaseClass(int i, int j) { this.i = i; this.j = j; } } class DerivedClass : BaseClass { int k;
public DerivedClass() : base(0, 0)
{
k = 0;
} public DerivedClass(int i, int j, int k) : base(i, j) { this.k = k; } }

You can create class with sealed modifier. Such class cannot be used as base class for creation of descendants. Methods with sealed modifier cannot be overridden.

You can implicitly convert reference to derived object to base class type, but not vice versa.

BaseClass b = new DerivedClass();
DerivedClass d = new DerivedClass();
b = d; // OK
d = b; // Compile error!

You can realize reverse conversion explicitly. If conversion cannot be done, an exception will be thrown, System.InvalidCastException. If you want to avoid exception handling, you can use as operator.

BaseClass b1 = new DerivedClass();
BaseClass b2 = new BaseClass();
DerivedClass d1 = b1 as DerivedClass; // OK
DerivedClass d2 = b2 as DerivedClass; // null

The is operator returns true if type cast is possible and false otherwise:

BaseClass b1 = new DerivedClass();
BaseClass b2 = new BaseClass();
if (b1 is DerivedClass)
{
    DerivedClass d1 = (DerivedClass) b1; // OK
}
if (b2 is DerivedClass)
{
    DerivedClass d2 = (DerivedClass) b2; // not executed
}

The as and is operators are imported form Delphi Pascal.

2.5 Polymorphism. Interfaces

The runtime polymorphism is a feature of classes, according to which the behavior of objects are determined not at compile time but at runtime. The concept of polymorphism and the polymorphic classes is generally common in all object-oriented programming languages, especially in C#.

All classes of C# are polymorphic, since they are derived from polymorphic class System.Object. Such other object-oriented programming languages, C# provides polymorphism based on virtual methods mechanism. You must define virtual methods using virtual keyword in the base class and override keyword in derived classes. If you want to overload virtual method and break chain of polymorphism, you should use new keyword before function header.

class Shape
{
    public virtual void Draw()
    {
        . . .
    }
}

class Circle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

class Rectangle : Shape 
{
    public new void Draw()
    {
        . . .
    }
}

Often there is a need for override virtual methods of System.Object class. For example, in order to represent the object as a string, you need to define ToString() method, which returns necessary string. This representation can be used for any purpose, such as to display all the information about the object by using Console.WriteLine():

class MyClass
{
    int k;
    double x;

    public MyClass(int k, double x)
    {
        this.k = k;
        this.x = x;
    }

    public override string ToString()
    {
        return k + " " + x;
    }

    static void Main(string[] args)
    {
        MyClass mc = new MyClass(1, 2.5);
        Console.WriteLine(mc);
    }
}    

You can also override the Equals() method, which allows you to compare objects together.

Sometimes classes are defined for representation of abstract concepts, not for instantiation. Such concepts can be represented using abstract classes. C# uses abstract keyword for definition of an abstract class.

abstract class SomeConcept 
{
    . . .
}

Abstract class can contain abstract methods, which are declared without implementation. To declare abstract methods, abstract specifier is used. Such methods have no body, only header followed by semicolon. Abstract method is virtual by default, but you cannot use virtual specifier. Method that override abstract method, must have override modifier.

For example, abstract class called Shape implements fields and methods, which can be used by derived classes. Such elements are, current position (fields), method MoveTo() that moves shape to a new position, etc. Abstract Draw() must be overridden in derived classes in different ways. The Draw() method does not need default implementation:

abstract class Shape 
{
    int x, y;
    . . .
    public void MoveTo(int newX, int newY) 
    {
        . . .
        Draw();
    }
    public abstract void Draw();
}

Specific classes derived from Shape, such as Circle or Rectangle, define their own implementation of Draw() method.

class Circle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

class Rectangle : Shape 
{
    public override void Draw()
    {
        . . .
    }
}

You can create abstract class without abstract methods. However, if you add at least one abstract method to class definition, this class must be declared with abstract specifier.

Abstract classes can declare abstract properties:

abstract class Shape
{
    public abstract double Area 
    {
        get;
    }
}

...

class Rectangle : Shape 
{
    double width, height;

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

    public override double Area   
    {
        get    
        {
            return width * height;
        }  
    }
}

Note: not to be confused with automatic abstract properties those have a similar syntax.

C# supports concept of interfaces. Interface can be interpreted as pure abstract class, which contains only declarations of abstract methods and properties:

interface Int1 {
    void f();
    int g(int x);
}

In fact, an interface is not a data type and in fact only declares certain behavior that must be provided by the class that implements the interface.

Each C# class can be derived from the only base class, but it can implement several interfaces. Class that implements some interface must implement all methods declared in this interface. Otherwise, this class is abstract and needs appropriate specifier.

Interfaces implemented by some class are listed after base class and are separated by commas. By convention, interface names starts from capitalized I letter to distinguish them from class names.

Methods declared in interface are abstract and public by default. Appropriate methods of a class that implements interface must be defined with public specifier.

interface ISomeFuncs {
    void F();
    int G(int x);
}

class SomeClass : ISomeFuncs {
    public void F()
    {

    }

    public int G(int x) 
    {
        return x;
    }
}

Class can implement several interfaces:

interface IFirst 
{
    void F();
    int G(int x);
}

interface ISecond
{
    void H(int z);
}

class AnotherClass : IFirst, ISecond 
{
    public void F() 
    {
  
    }

    public int G(int x) 
    {
        return x;
    }

    public void H(int z) 
    {
  
    }
}

You can create derived interfaces. Multiple inheritance can be used for interfaces:

interface IFirst 
{
    void F();
    int G(int x);
}

interface ISecond
{
    void H(int z);
}

interface IDerived : IFirst, ISecond 
{

}

Interface can contain declarations of properties. You can also define constants within interfaces.

There is ability of explicit definition of interface methods. This is reasonable if some class implements several interfaces with matching method names and parameter lists.

type_name Interface_name.metod_name() 
{ 
    ...// implementation
}

Such methods do not allow specification of visibility. The invocation of these methods is only allowed for references to interface (not for references to implementing class).

Starting from C# 8 you can define default implementation for interface methods. The goal is to allow adding new methods to previously defined interfaces without modification of classes that implemented previous versions of interfaces. But there are differences in the definition and usage of methods with default implementation. You can define method without any modifiers. You can also define static methods:

public interface IGreetings
{
    void Hello()
    {
        Console.WriteLine("Hello world!");
    }
    static void HelloStatic()
    {
        Console.WriteLine("Hello as well!");
    }
}

The non-static methods can be invoked only through reference to interface (not through reference to implementing class):

class Greetings : IGreetings
{
    public void TestHello()
    {
        IGreetings greetings = this;
        greetings.Hello();
    }
}

class Program
{
    static void Main(string[] args)
    {
        new Greetings().TestHello();
    }
}

Static methods can be invoked through interface name:

class Program
{
    static void Main(string[] args)
    {
        IGreetings.HelloStatic();
    }
}

You can override default implementations in derived interfaces. Such methods can be also redeclared as abstract ones:

public interface IMyGreetings : IGreetings
{
    void IGreetings.Hello()
    {
        Console.WriteLine("Hello to me!");
    }
}

public interface IAbstractGreetings : IGreetings
{
    abstract void IGreetings.Hello();
}

Now this method must be realized in classes that implement IAbstractGreetings interface.

2.6 Anonymous Types and Records

Starting with version C# 3, you can create local variables of so-called anonymous types. For example:

var city = new { Name = "Kyiv", Population = 2884000 };
Console.WriteLine(city.Name);

An anonymous type is a reference type. The values that were defined when the variable was created are read-only and cannot be changed. Methods, events, and elements other than properties cannot be added to anonymous types. For anonymous types, the Equals() method is defined, which checks the equivalence of all properties.

In order to pass an object of an anonymous type to a function as an argument, the parameter must be described as object.

var city = new { Name = "Kyiv", Population = 2884000 };
PrintCity(city);

. . .

void PrintCity(object city)
{
    Console.WriteLine(city); // { Name = Kyiv, Population = 2884000 }
}

The possibilities of using such parameters are significantly limited, because the variable cannot be explicitly converted to an anonymous type. Therefore, it is desirable to use variables of anonymous types in the same block where they are defined.

The record type introduced in version C# 9 is a simplified class, in many respects similar to anonymous types. It is also usually created to represent immutable objects. For example, you can describe the following record:

public record City
{
    public string Name { get; init; }
    public int Population { get; init; }
}

The init keyword indicates that the automatic property can be assigned in the constructor or in the initializer, but then its value cannot be changed. Now we have a type for immutable objects.

Note: The init keyword in the description of automatic properties, introduced in C# 9, can be used not only to describe properties of records, but also in regular classes.

You can now create record type objects.

var city = new City() { Name = "Kyiv", Population = 2884000 };

Records support inheritance. You can create constructors. Automatic equivalence check is supported.

2.7 Structures and Enumerations

Structure in C# looks like class. Structure can contain fields and methods, but it is not a reference type. Structures are always value types. Structures are suitable to represent small objects. In addition, the structure is always created in the programming stack, even if it was created using new operator. Placing within the stack increases the effectiveness of the program. Structure can contain constructors. Automatically created parameterless constructor initializes all fields by default values for the corresponding types.

public struct Point 
{
    public double x, y;
    public Point(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
}

Previous versions of C# (until version C# 9) you could not initialize data in the body structure. It was not possible to redefine default constructor. Version C# 10 allows you to define constructor without parameters that provides field initialization with the required values.

You cannot assign null value to structures (if they are non-nullable). Garbage collector does not remove struct objects from memory.

You can create objects of struct type using new operator. This guarantee assignment of default values to fields. Otherwise, fields are not initialized and their values cannot be used without assignment:

Point p1 = new Point();
double d1 = p1.x; // d1 == 0
Point p2;
double d2 = p2.y; // Compile error

Initialization and assignment operations effect full copying of field values:

Point p1 = new Point();
Point p2 = p1; // copying

Structures are transferred to functions by value. To transfer them by reference ref and out modifiers are used.

All structure types are derived from System.ValueType that is a type derived from System.Object. But you cannot apply inheritance mechanism to structures, only implementation of interfaces is allowed. You can override methods of System.Object, e.g. ToString(). Overriding is carried out with the use of the overrіde modifier, which in other cases is prohibited for structures.

Because structures are value types, when they are used where a reference to a class object is expected (for example, in assigning them to an object of type System.Object), their so-called boxing into a reference type occurs. The memory in the free store is allocated, to which all the data are copied, and a reference to this area is returned. During the inverse conversion, "unboxing" takes place: the data is copied to an instance of the structure:

Point p = new Point(1,2);
object ob = p;         // boxing, make a copy
Point p2 = (Point) ob; // unboxing

Starting with version C# 10, record objects can also be allocated in the stack. To describe such records pair of words record struct is used.

Enumeration in C# is a type for representation of a set of named constants. For example:

enum Digits 
{ 
    zero, 
    one, 
    two, 
    three 
}   // zero == 0, one == 1, two == 2, three == 3

By default, first constant is 0, and other are defined by adding 1 to previous one. You can set some values explicitly. The values of followed constants are defined using the same rule:

enum Digits
{ 
    one = 1,
    two,
    three
} // two == 2, three == 3

You can use enumeration elements for definition of other elements:

enum Digits 
{ 
    one = 1,
    two,
    min = one,
    max = two
}

To access elements of enumeration, type name followed by constant name. For example: Digits.two. You can declare variables and constants of enumeration type:

Digits d = Digits.one;

To obtain integer value of an element of enumeration, explicit type cast is needed:

int c = (int) Digits.two; // c == 2

The System.Enum class provides several useful methods for working with enumerations. This class is the base type for all enumerations.

The default type of constants that can be stored in enumerations is int. However, this type (base type of enumeration) can be changed. For example:

enum Digits : byte 
{ 
    one = 1, 
    two, 
    three 
} // two == 2, three == 3

To work with enumerations you can use static methods of System.Enum type. In particular, the method GetUnderlyingType() returns a data type that is used to store the values of enumeration, GetValues() returns an array of values. For example:

enum Digits : byte 
{ 
    one = 1, 
    two, 
    three 
}
  
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(Enum.GetUnderlyingType(typeof(Digits)));
        foreach (var v in Enum.GetValues(typeof(Digits)))
        {
            Console.Write(v + " "); // one two three
        }
    }
}    

In this example, typeof operator returns an object of System.Type. Its overloaded ToString() method allows you to get type name.

2.8 Tuples

Starting from C# 7.0 you have possibility of easy creation the so called tuples. A tuple is a finite ordered sequence of elements. Unlike classes and structures, tuples support the so-called "lightweight" syntax: they can be used as groups of ordered data. The explicit usage of inheritance and polymorphism is not allowed. In the simplest case we can create a tuple in the following way (the so called unnamed tuple):

var unnamedPair = (1, 2);

You can get items from a tuple using reserved member names Item1, Item2, etc.

Console.WriteLine(unnamedPair.Item1);
Console.WriteLine(unnamedPair.Item2);

The better approach assumes usage of named tuples:

var namedPair = (First: 1, Second: 2);
Console.WriteLine(namedPair.First);
Console.WriteLine(namedPair.Second);

Types of tuple items are extracted from initial values. You can define them explicitly at creation of variables:

(int, int) unnamedPair = (1, 2);
(int First, int Second) namedPair = (First: 1, Second: 2);

You can also use values of variables for initialization of tuples:

var integer = 1;
var real = 1.5;
var tuple = (First: integer, Second: real);
Console.WriteLine($"{tuple.First} {tuple.Second}");

Tuples can be used as function arguments:

static int Sum((int, int) args)
{
    return args.Item1 + args.Item2;
}

static void Main(string[] args)
{
    Console.WriteLine(Sum((1, 2)));
}

The one of most essential benefits of tuples is the possibility of returning several values from a function. The following example demonstrates usage of tuples for returning three values from a function. This function solves quadratic equation. The first value of bool type is true if equation can be solved. Two other values are roots of an equation:

static (bool Solvable, double? X1, double? X2) SolveQuadratic(double a, double b, double c)
{
    if (a == 0 || D() < 0)
    {
        return (false, null, null);
    }
    return (true, X(-1), X(+1));
    double D()
    {
        return b * b - 4 * a * c;
    }
    double X(int k)
    {
        return (-b + k * Sqrt(D())) / (2 * a);
    }
}

static void Main(string[] args)
{
    var result = SolveQuadratic(1, 2, 1);
    Console.WriteLine(result.Solvable + " " + result.X1 + " " + result.X2); // True -1 -1
    result = SolveQuadratic(1, 2, 3);
    Console.WriteLine(result.Solvable + " " + result.X1 + " " + result.X2); // False
}

Note: this example also shows the usage of local functions.

2.9 Methods that Extend Existing Types

Sometimes you need to add new methods to the previously created class. Traditionally, there are three ways to solve this problem:

  • modification of the source code. Of course, this approach cannot be correct. Furthermore, the modification of the source code can be impossible, for example, in the case of .NET standard classes and structures, or even when we use the classes provided in compiled form;
  • creation of derived class, which provides necessary methods. This approach has numerous limitations. For example, overloaded operators cannot be applied to objects of derived classes, hence the corresponding operator functions should be defined again. In addition, derived classes are not part of the .NET class library and their names cannot be C# keywords, (like string). But the most important is that structures do not support inheritance mechanism;
  • creation of a static function with parameter of type that we want to expand. This is the correct approach, but it is associated with some inconveniences. In particular, outside of class, which identified these features, you must use the appropriate prefix.

C# 3.0 (and later versions) provides the ability to add new methods to existing classes and structures. To add a new method (for example, called newMethod), follow these steps:

  • create a new static class
  • add a new static function (newMethod); the first argument of a new method must be reference to object of type (class or structure), to which we want to add a new method; the this modifier must precede the definition of the first argument.

Now you can use this function as a non-static method applied to objects of desired type. For example, you can extend the int type with method that triples the corresponding integer value. You should create a new static class:

namespace Extensions
{
    public static class IntExtensions
    {
        public static int Triple(this int k)
        {
            return k * 3;
        }
    }
}

You can call the Triple() method for variables and constants to the appropriate type:

int n = 2;
int m = n.Triple();
int k = 9.Triple();

The Triple() method can be invoked in static way:

int q = IntExtensions.Triple(m);

Visibility of the Triple() method is limited to the current namespace.

You can create methods with multiple arguments. For example:

public static class DoubleExt
{
    public static double Add(this double a, double b)
    {
        return a + b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        double x = 2.5.Add(3);
        Console.WriteLine(x); // 5.5
    }
}    

2.10 Nested Types

C# allows creation of nested types. Typically, these are nested classes.

You can apply visibility directives to nested classes. Public nested classes can be used outside the outer class, but private can be used only inside. From the outer class there is no access to the private members of nested classes.

  public class Outer
  {
      static int i = 12;

      public class FirstInner
      {
          public void F()
          {
              i = 10;
          }
      }

      private class SecondInner
      {
          public int z;
          private int x;
      }

      SecondInner si = new SecondInner();

      public void g()
      {
          si.z = 12;
          si.x = 13; // Error!
      }
  }

  class Program
  {
      static void Main(string[] args)
      {
          Outer.FirstInner first = new Outer.FirstInner();
          first.F();
          Outer.SecondInner s; // Error!
      }
  }

You cannot create local and anonymous classes. You can create nested structures and enumerations. For example:

struct Polyline
{
    public struct Point
    {
        public double X, Y;
    }
    public Point[] Points { get; set; }
}    

You can also create nested types within interfaces.

2.11 Pattern Matching

The concept of pattern matching is an advance of the idea of implementing branching algorithms. At the general level, pattern matching involves the execution of program code, depending on the coincidence of the checked value with a particular pattern. Depending on the capabilities of the programming language, it can be

  • constant
  • predicate
  • data type
  • another construction of the programming language

Traditional means of comparing the values of variables with constants in the statements if and switch are the simplest forms of comparison with the sample. When C# is about mapping to a sample, we mean the object type validation constructs added starting from C# 7.0, while creating a reference to a variable of the appropriate type. For example, we have a variable

Traditional means of comparing variable values to constants in if and switch statements are the simplest forms of pattern matching. When it comes to pattern matching in C#, it means that starting from C# 7.0, constructs for checking object types have been added, with creating a reference to a variable of the appropriate type. For example, we have a variable:

object obj = "Text";

Somewhere in the program you should check if this variable really refers to the string, and perform certain actions. Prior to the introduction of new constructions related to pattern matching, explicit type conversion had to be performed:

if (obj is string)
{
    string s = (string)obj;
    Console.WriteLine(s.Length);
}

Starting with version C# 7.0, you can use a more compact construct:

if (obj is string s)
{
    Console.WriteLine(s.Length);
}

But the most interesting innovation is the use of type checking in the switch() statement. For example:

switch (obj)
{
    case string s:
        Console.WriteLine(s.Length);
        break;
    case int i:
        Console.WriteLine(i + 1);
        break;
    default:
        Console.WriteLine("Wrong type");
        break;
}

In sample matching expressions, you can use the when construct to define an additional condition, for example:

switch (obj)
{
    case string s when s.Length == 0:
        Console.WriteLine("Empty string");
        break;
    case string s:
        Console.WriteLine(s);
        break;
    default:
        Console.WriteLine("Wrong type");
        break;
}

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

3 Sample Programs

3.1 Hierarchy of Real World Objects

Our goal is to create the following class hierarchy:

  • Region
  • Populated Region
  • Country
  • City
  • Island

Several classes of this hierarchy can be used as base classes for other classes (for instance, "Uninhabited island", "National park", "Borough", etc.). Each class should provide its original constructor for fields' initialization. Array of references to base object can be filled with references to objects of different derived types.

To obtain string representation of some object, you should override ToString() method. The class hierarchy can be as follows:

namespace HierarchyTest
{
    // Class hierarchy
    class Region {
        public string Name { get; set; }
        public double Area { get; set; }

        public Region(string name, double area) 
        {
            Name = name;
            Area = area;
        }

        public override string ToString() 
        {
            return Name + ".\tArea " + Area + " sq.km.";
        }
  
    }

    class PopulatedRegion : Region {
        public int Population { get; set; }

        public PopulatedRegion(string name, double area, int population) 
              : base(name, area)
        {
            Population = population;
        }

        public int Density()
        {
            return (int) (Population / Area);
        }

        public override string ToString()
        {
            return base.ToString() + 
                   "   \tPopulation " + Population + " inhab.\tDensity" +
                   Density() + " inhab. per sq.km.\n";
        }

    }

    class Country : PopulatedRegion 
    {
        public string Capital { get; set; }

        public Country(string name, double area, int population, string capital) :
              base(name, area, population) 
        {
            Capital = capital;
        }

        public override string ToString()
        {
            return "Country  " + base.ToString() + ".\tCapital " + Capital + "\n";
        }

    }

    class City : PopulatedRegion 
    {
        public int Boroughs { get; set; } // Count of boroughs
        public City(string name, double area, int population, int boroughs) :
              base(name, area, population) 
        {
            Boroughs = boroughs;
        }

        public override string ToString()
        {
            return "City " + base.ToString() + " " + Boroughs + " boroughs\n";
        }
  
    }

    class Island : PopulatedRegion 
    {
        public string Sea { get; set; }

        public Island(string name, double area, int population, string sea) :
            base(name, area, population) 
        {
            Sea = sea;
        }

        public override string ToString()
        {
            return "Island " + base.ToString() + "Sea:     " + Sea + "\n";
        }  
    }

    class Program
    {
        static void Main(string[] args)
        {
            Region[] a = { new City("Kiev", 839, 2679000, 10),
                           new Country("Ukraine", 603700, 46294000, "Kiev"),
                           new City("Kharkov", 310, 1461000, 9),
                           new Island("Zmiyiny", 0.2, 30, "Black Sea") };
            foreach (Region region in a)
            {
                System.Console.WriteLine(region);
            }
        }
    }
}

3.2 Finding a Minimum using Dichotomy Method

Suppose we want to create a universal class for finding the minimum of any function f(x) using dichotomy method. Algorithm for finding a minimum at a certain interval [a, b] with precision h is as follows:

  • determined mean of the interval (x)
  • calculated values of f(x - h) and f(x + h)
  • if f(x - h) > f(x + h), the beginning of the interval moved to x, otherwise transferred the end of the interval moved to x
  • if the length of the new interval is less than h, the process terminates, otherwise all repeats for the new interval.

Note that this algorithm only works for the case when we get an interval with exactly one minimum.

We can offer two approaches: with abstract classes and with interfaces.

The First Version

We can create an abstract class (AbstractMinimum) containing an abstract F() method and the function of the minimum (Find()). In a derived class, this function F() should be overridden.

using System;

namespace LabSecond
{
    public abstract class AbstractMinimum {
        abstract public double F(double x);

        public double Solve(double a, double b, double h)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > h) {
                if (F(x - h) > F(x + h))
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }

    class SpecificMinimum : AbstractMinimum 
    {
         public override double F(double x)
        {
            return x * x - x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SpecificMinimum sm = new SpecificMinimum();
            Console.WriteLine(sm.Solve(0, 3, 0.000001));
        }
    }
}

The Second Version

Now we define interface for the representation of some function. The Finder class implements a static method for finding minimum. Another class that implements our interface contains a specific implementation of F().

using System;

namespace LabSecond
{
    public interface IFunction
    {
        double F(double x);
    }

    public class Solver
    {
        public static double Solve(double a, double b, double h, IFunction func)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > h)
            {
                if (func.F(x - h) > func.F(x + h))
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }

    class MyFunc : IFunction {
        public double F(double x) {
            return x * x - x;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 3, 0.000001, new MyFunc()));
        }
    }
}

3.3 The Hierarchy of Bookshelves

Suppose we want to expand previously created a program that describes the bookshelf, adding a hierarchy of bookshelves. For example, a particular shelf type is named shelf. Both classes should provide overloaded operators for adding books (+) and removal books (-). All classes also need to override ToString() method, Book and Author types also need overloading of Equals() method. Separate structure is needed for To represent the author.

Here is the full text of the program:

using System;

namespace LabSecond
{
    // Structure that represents an author
    public struct Author
    {
        public string Surname, Name;

        // 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
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public Author[] Authors { get; set; }

        // Constructor
        public Book(string title, int year, params Author[] authors)
        {
            Title = title;
            Year = year;
            Authors = authors;
        }

        // Definition of string representation
        // string.Format() provides a format similar to Console.WriteLine()
        public override string ToString()
        {
            string s = string.Format("Title: \"{0}\". Year of publication: {1}", Title, Year);
            s += "\n" + "   Authors:";
            for (int i = 0; i < Authors.Length; i++)
            {
                s += string.Format("      {0}", Authors[i]);
                if (i < Authors.Length - 1)
                {
                    s += ",";
                }
                else
                {
                    s += "\n";
                }
            }
            return s;
        }

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj is Book b)
            {
                if (b.Authors.Length != Authors.Length)
                {
                    return false;
                }
                for (int i = 0; i < Authors.Length; 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
    {
        public Book[] Books { get; set; }

        // Constructor
        public Bookshelf(params Book[] books)
        {
            Books = books;
        }

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

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

        // Looking for books with a certain sequence of characters
        public Book[] ContainsCharacters(string characters)
        {
            Book[] found = Array.Empty<Book>();
            foreach (Book book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Add a new item:
                    Array.Resize(ref found, found.Length + 1);
                    found[^1] = book;
                }
            }
            return found;
        }

        // Adding book
        public void Add(Book book)
        {
            Book[] books = Books;
            Array.Resize(ref books, Books.Length + 1);
            Books = books;
            Books[^1] = book;
        }

        // Removal of a book with the specified data
        public void Remove(Book book)
        {
            int i, k;
            Book[] newBooks = new Book[Books.Length];
            for (i = 0, k = 0; i < Books.Length; i++, k++)
            {
                if (Books[i].Equals(book))
                {
                    k--;
                }
                else
                {
                    newBooks[k] = Books[i];
                }
            }
            if (i > k)
            {
                Array.Resize(ref newBooks, Books.Length - 1);
            }
            Books = newBooks;
        }

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

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

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

        public TitledBookshelf(string title, params Book[] books) : base(books)
        {
            Title = title;
        }

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

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

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

    class Program
    {
        static void Main()
        {
            // Create an empty shelf:
            Bookshelf bookshelf = new();

            // Adding books
            bookshelf += new Book("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("Pro C# 2010 and the .NET 4 Platform", 2010, 
                                  new Author() { Name = "Andrew", Surname =  "Troelsen" });
            bookshelf += new Book("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 newBookshelf = new(bookshelf.ContainsCharacters(sequence));
      
            // Output to the screen:
            Console.WriteLine("The found books:"); 
            Console.WriteLine(newBookshelf);
            Console.WriteLine();

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

            // Create a new shelf
            TitledBookshelf titledBookshelf = new TitledBookshelf("Java");
            titledBookshelf += javaBook;
            Console.WriteLine("New bookshelf:");
            Console.WriteLine(titledBookshelf);
        }
    }
}

4 Exercises

  1. Expand int type with method of calculating the square.
  2. Expand double type with method of calculating the third power.
  3. Expand int type with method of calculating factorial.
  4. Expand double type with method of calculating integer power.
  5. Expand System.String class with method of removal the first and the last characters.
  6. Expand System.String class with method of check whether first and last letters are the same .
  7. Create a class "Group of people". Implement operator overloading for adding (+) and removal (-) group members. Use object initializers.
  8. Create a Class "A simple fraction". Implement overloading for operations +, -, and *.
  9. Create a class hierarchy "Book" and the "Handbook". Implement constructors and access methods. Override ToString() method. In the Main() function create an array that contains elements of different types. Display elements on the screen.
  10. Create a class hierarchy "Movie" and "Serial". Implement constructors and access methods. Override ToString() method. In the Main() function create an array that contains elements of different types. Display elements on the screen.
  11. Create a class hierarchy of classes "City" and "Capital". Implement constructors and access methods. Override ToString() method. In the Main() function create an array that contains elements of different types. Display elements on the screen.
  12. Create a hierarchy of classes "Pet" and "Cat". Override ToString() method. In the Main() function create an array that contains items of different types. Display items on the screen.
  13. Create a class hierarchy "Planet" and "Satellite". Override ToString() method. In the Main() function create an array that contains elements of different types. Display elements on the screen.
  14. Create a class "Group" with nested structure for the representation of student and an array of students.
  15. Create a class "Bookcase" with nested structure for the representation of books and an array of books.
  16. Create an enumeration for representing days of week. Print all values on the screen.
  17. Create an enumeration for representing months. Print all values on the screen.

5 Quiz

  1. What are the advantages and disadvantages of object initializers in comparison with constructors?
  2. Is it possible to initialize private fields and properties by using object initializers?
  3. How can you create a static class?
  4. What are advantages and disadvantages of static classes?
  5. What is operator overloading?
  6. How to describe an operator function?
  7. Is it possible to call operator function instead of using the operator?
  8. What is the purpose of inheritance?
  9. What is the difference between single and multiple inheritance?
  10. What elements of the base class are not inherited?
  11. How do you initialize the base class?
  12. Where and what you can use base keyword?
  13. How to override method with sealed modifier?
  14. Can you implicitly cast base class reference to a derived class reference?
  15. What are the opportunities of polymorphism?
  16. What is the difference between virtual and non-virtual methods?
  17. How to define virtual methods?
  18. What is the compiler reaction on the lack of override or new keywords before the overridden virtual method?
  19. How to describe abstract methods and classes?
  20. Can abstract classes contain non-abstract methods?
  21. What are the advantages of interfaces versus abstract classes?
  22. What are differences in implementation of methods inherited from abstract classes in Java and C#?
  23. How to describe and implement an interface?
  24. Is it allowed multiple inheritance of interfaces?
  25. What is the difference between definition (usage) interfaces in Java and C#?
  26. What is the purpose of explicit interface implementation?
  27. What are the features of methods that explicitly implement interfaces?
  28. How to define and call an interface method with a default implementation?
  29. What is the reason of using anonymous types?
  30. What are the features of creating and using records?
  31. What are the advantages and disadvantages of structures?
  32. How to describe the structure?
  33. How to define default constructor for the structure?
  34. What are the features of creating structure type objects using new operator?
  35. What is structure boxing?
  36. How can we describe and use the enumeration?
  37. What is the difference between implementation of enumerations in Java and C#?
  38. Syntax and advantages of usage tuples.
  39. Can you add static methods to the previously created classes without modifying their source code?
  40. How to describe a method, which is added to an existing class?
  41. What is the difference between usage and implementation of nested classes in Java and C#?
  42. Is it possible to create nested class inside interface?

 

up