Laboratory Training 3
Inheritance and Polymorphism
1 Training Tasks
1.1 Individual task
Expand the program that was created in the previous laboratory training with a hierarchy of entities that are stored in the array, according to the following table:
Base Class | Derived Class | Derived Class |
---|---|---|
Student | Budget student | Contract student |
Educational subject | Subject with an exam | Credit subject |
Part of the city | Ward | The historical part of the city |
Settlement | Town | Village |
Participant of the sports section | Member | Head |
A member of a football club | Player | Coach |
A member of a musical group | Member | Head |
Collection of songs | Album | Collection of sheet music |
Music | Song | Instrumental work |
Housing | Room | Service room |
Work | Story | Novel |
Work of art | Painting | Graphics |
The subway facility | Station | Depot |
Railway object | Station | Stop |
Work | Novel | A play |
Reproduce tasks of previous laboratory training. It is also necessary to override the ToString()
method
for all classes.
Provide methods for adding and removing an entity from an array.
Implement the functions of saving data in a text file and loading data from a text file. Reproduce tasks of previous laboratory trainings.
Implement handling possible exceptions.
1.2 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.3 Working with Text Files
Develop a program that performs copying from one file to another file only strings whose length is less than some integer value.
2 Instructions
2.1 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.
Thanks to inheritance and the creation of class hierarchies, it is possible to significantly reduce the number of fragments of similar code present in different classes, which would be a violation of one of the fundamental principles of programming, the principle of DRY ("Don't Repeat Yourself"). In addition, inheritance allows you to implement one of the principles of SOLID - the Open / Closed Principle (OCP), according to which classes should be open for extension, but closed for modification.
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 Rectangle : Shape { // 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.
One of the principles of SOLID is related to inheritance - the Liskov substitution principle (LSP), according to
which objects can be replaced by their descendants without changing the code. Inheritance should not limit the functionality
of base classes. For example, the Square
class should not be created as a derivative of the Rectangle
class,
since it is actually a narrowing of the type, not an extension of the type. Where a program requires any rectangle,
you cannot use a square because you cannot change the height and width of such a rectangle separately. More correct
derived classes from the "Rectangle" class are "Colored rectangle", "Rectangle with rounded corners", "Rectangle
with text", etc.
Private and protected inheritance (types of inheritance introduced in C++ and absent in other object-oriented programming languages) are incompatible with the LSP principle.
2.2 Polymorphism. Interfaces
2.2.1 Runtime Polymorphism
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 mechanism of virtual
methods. 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); } }
2.2.2 Abstract Classes
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. Access code is not specified for such properties, but only the need to assign such code in descendants is indicated. For example:
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
2.2.3 Interfaces
C# supports concept of interfaces. Interface can be interpreted as an abstract class, which contains methods and properties. These methods are abstract by default (unless their implementation is given):
interface Int1 { void F(); int G(int x); int P { get; set; } }
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.
When designing type hierarchies, there is a temptation to create large "universal" interfaces that describe almost all the functionality of a complex system. When designing such interfaces, one should think about the difficulties associated with their implementation. In addition, how it provokes the creation of unwanted mutual dependencies and connections. One of the principles of SOLID - the Interface Segregation Principle (ISP) determines the feasibility of creating a large number of specialized interfaces instead of one universal one, which increases the flexibility of software components, removes unnecessary dependencies and simplifies implementation.
2.2.4 Use of Standard Interfaces
There are a large number of standard interfaces. Some of them are integrated into the C# syntax. For example, an
interface IDisposable
declares a method Dispose()
whose purpose is to perform some final operation, such as closing
files, freeing other resources, etc. In order for this method to be guaranteed to be called, special using
construct
can be applied:
using (X x = new X()) { // work with x object } // call of Dispose()
As mentioned earlier, arrays of numeric items are sorted in ascending order. The CompareTo()
method of IComparable
interface
can be defined in classes and structures (that implement IComparable
interface) for setting default
sort order. This method should return a negative value (e.g. -1) if this object less than argument, 0 if objects
are equal, and a positive value otherwise. The elements of arrays (lists) should be objects that implement IComparable
interface.
You can choose to create a class that implements IComparable
interface. For example, an array of rectangles
can be sorted by area:
class Rectangle : IComparable<Rectangle> { double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double Area() { return width * height; } public int CompareTo(Rectangle rect) { return Area().CompareTo(rect.Area()); } public override String ToString() { return "[" + width + ", " + height + ", area = " + Area() + "]"; } } class Program { static void Main(string[] args) { Rectangle[] a = {new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4)}; Array.Sort(a); foreach (Rectangle rect in a) Console.WriteLine(rect); } }
If you attempt to sort items that do not implement the IComparable
interface, you obtain InvalidOperationException
.
If you do not want (or cannot) determine the CompareTo()
function, you can create a separate class
that implements the interface IComparer
. Reference to this class object are passed as the second parameter
in the function Sort()
for arrays (or the first option for lists). IComparer
interface
the method Compare()
with two parameters. The function should return a negative number if the first
object should be considered less than another, zero if objects are equivalent, and a positive number otherwise.
class Rectangle { double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } public double Area() { return width * height; } public override String ToString() { return "[" + width + ", " + height + ", area = " + Area() + "]"; } } class CompareByArea : IComparer<Rectangle> { public int Compare(Rectangle r1, Rectangle r2) { return r1.Area().CompareTo(r2.Area()); } } class Program { static void Main(string[] args) { Rectangle[] a = { new Rectangle(2, 7), new Rectangle(5, 3), new Rectangle(3, 4) }; Array.Sort(a, new CompareByArea()); foreach (Rectangle rect in a) { Console.WriteLine(rect); } } }
Sorting lists (List
type) is done similarly. Lists will be considered in the next laboratory training.
Some other standard interfaces will be discussed below in the context of their application.
2.2.5 Inheritance and Polymorphism when Working with Records, Structures and Tuples
Records support inheritance. For example:
public record PopulatedRegion { public string Name { get; init; } = ""; public double Area { get; init; } public int Population { get; init; } } public record Country : PopulatedRegion { public string Capital { get; init; } = ""; }
All structs are explicit descendants of the System.ValueType
that is descendant of System.Object
.
However, structures do not support the mechanism of explicit inheritance, although they can implement interfaces.
You can override methods of System.Object
class, for example, such as ToString()
. Overriding
is carried out using the override
modifier, which is otherwise prohibited
for structures.
Explicit use of inheritance and polymorphism for tuples is not supported.
2.3 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 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.4 Reflection
Reflection is a mechanism that allows the program to monitor and modify its own
structure and behavior at runtime. Information about the types that can be obtained through the mechanism of reflection
is contained in the metadata of the assembly. The C# language provides classes Assembly
, MethodInfo
, PropertyInfo
, FieldInfo
and
other types of System.Reflection
namespace.
At runtime, type information can be obtained by specifying a string with the full type name (including namespaces and nested namespaces):
int k = 100; Type type = Type.GetType("System.Int32"); MemberInfo[] members = type.GetMembers(); foreach(MemberInfo member in members) { Console.WriteLine(member); }
The result of the program will be a relatively large list of fields, methods and properties (including static members)
defined in the System.Int32 structure
and its basic types.
You can also create a variable to get type information.
int k = 100; Type type = k.GetType(); Console.WriteLine(type); // System.Int32
You can get separate information about methods, fields, and properties:
FieldInfo[] fields = type.GetFields(); MethodInfo[] methods = type.GetMethods(); PropertyInfo[] properties = type.GetProperties();
You can use reflection to create objects of types whose names are defined by a string. You
can call methods, work with fields and properties through the names defined at runtime. For example, you
can create an instance of a specific class (MySpace.MyClass
) and load its call method (MyFunc
):
// Create an instance (object) of the class: object o = assembly.CreateInstance("MySpace.MyClass"); // Get information about the type: Type t = o.GetType(); // Get information about the method with the specified name: MethodInfo mi = t.GetMethod("MyFunc"); // Call the method with parameter x: object result = mi.Invoke(o, new object[] { x });;
C# reflection tools allow you, in addition to public data, properties, and methods, to obtain information about private type members. For example, you can get information about all fields of some type:
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
Since through the mechanisms of reflection it is possible not only to receive information, but also to change the values of fields, call methods, etc., reflection actually allows you to bypass the limitations of encapsulation.
2.5 Exception Handling
Using exceptions handling mechanism is a very important part of programming on all modern object-oriented languages. Exceptions allow programmer to separate points where runtime errors occur from points of error handling. An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions.
To generate exception, throw
operator is used. After the throw
keyword, you should
place object of System.Exception
class or classes derived from it. These derived classes reflect the
specifics of a particular program.
class SpecificException : Exception { }
The System.Exception
class contains a number of properties that you can use to access information
about the exception, including:
Message
– text description of the error, defined as the constructor parameter when creating the exception object;Source
– name of the object or application that throws an exception;StackTrace
– sequence of calls that resulted in the error.
In most cases, an exception object is created at the point of throwing exception by using new
operator,
but sometimes an exception object can be created before. Typical throw statement might look like this:
void F() { . . . if (/* error */) throw new SpecificException(); . . . }
The function header does not specify types of exceptions thrown by this function. In the following example, the Reciprocal()
function
throws an exception in the case of division by zero.
class DivisionByZero : Exception { } class Test { public double Reciprocal(double x) { if (x == 0) { throw new DivisionByZero(); } return 1 / x; } }
Unlike C++, C# does not allow the creation exceptions of primitive types. Only instances of classes derived
from Exception
are allowed.
The try
block contains exception-prone code:
double x, y; . . . try { y = Reciprocal(x); }
A try
block is followed by a sequence of one or more catch
statements,
or handlers, each of which handles a different type of exception. The catch
block without brackets
handles all other exceptions:
catch (DivisionByZero d) { // handling exception } catch (SpecificException) { // handling exception } catch { // handling exception }
As shown in the example, you can omit object identifier in the header of catch
block,
if only type is important.
Exceptions form an object hierarchy, so a particular exception might match more than one catch
block.
What you have to do here is put catch
blocks for the more specific exceptions before those for
the more general exceptions.
In some cases, exceptions handler cannot ultimately handle the exception and should submit it to outer handler.
This can be done by using the throw
statement:
catch (SomeEx ex) { // local exception handling throw (ex); // rethrowing }
You can use throw
without expression, if exception object was not specified:
catch (Exception) { // local exception handling throw; }
After the last catch
block you can place finally
block. This code is always
executed regardless of whether an exception occurred or not.
try { OpenFile(); // other activities } catch (FileError f) { // handling exception } catch (Exception ex) { // handling exception } finally { CloseFile(); }
.NET defined standard exceptions. These classes are also descendants of Exception
. One of the most
frequently occurring standard exception is System.NullReferenceException
, which is thrown when you
try to access class members by reference, which equal to null
. The System.IndexOutOfRangeException
is
thrown when you try wok with improper array index.
There are also inner .NET exceptions that signal a serious problem during the execution of the program and may
arise in any part of your program. These are ExecutionEngineException
(internal CLR error), StackOverflowException
, OutOfMemoryException
,
etc. Typically, these exceptions are not caught.
Starting from version 6 of the C# language, you can add the so-called exception filters to catch
statements.
Filter expressions after when
keyword determine when a given catch
clause
should be applied. If this expression is true
, the catch clause executes normally.
Otherwise, the catch
clause is skipped. For example:
static void SomeFunc(int k) { if (k == 1) { throw new Exception("First case"); } if (k == 2) { throw new Exception("Second case"); } throw new Exception("Other case"); } static void Main(string[] args) { int n = int.Parse(Console.ReadLine()); try { SomeFunc(n); } catch (Exception ex) when (ex.Message.Contains("First")) // Exception filter { Console.WriteLine("Our case!"); } catch { Console.WriteLine("Something else"); } }
If this expression is put into catch
block's code, it means that an exception is
processed and rethrown. Usage of exception filters assumes that expression is not processed at all. Exception filters
can be also used for debugging.
2.6 Initial Information about Working with Files
2.6.1 Working with Text Files
As almost all universal programming languages, C# provides means of working with files and other streams. These
facilities are described in the System.IO
namespace. The classes in this namespace offer a number of
methods for creation streams, reading, writing, etc. Streams for working with text are called character streams.
Base classes of all character streams are TextReader
and TextWriter
. Derived classes StreamWriter
and StreamReader
,
and their derivatives, provide work with text files.
The following program performs reading lines from a text file and writes them into another text file.
using System; using System.IO; namespace LabThird { class Program { static void Main(string[] args) { using (StreamReader reader = new StreamReader("From.txt", Encoding.Default)) { using (StreamWriter writer = new StreamWriter("To.txt")) { string s; while ((s = reader.ReadLine()) != null) { writer.WriteLine(s); } } } } } }
Creation of stream objects within using
block causes automatic call of Dispose()
methods,
which in turn invoke Close()
functions. The From.txt file must be placed into bin\Debug or bin\Release folder
of project before execution (depending on solution configuration).
The ReadToEnd()
method allows you to read the entire file to the end and puts entire contents into
a single string. This function allows reduction of the previous program:
using System; using System.IO; namespace LabThird { class Program { static void Main(string[] args) { using (StreamReader reader = new StreamReader("From.txt", Encoding.Default)) { using (StreamWriter writer = new StreamWriter("To.txt")) { string s = reader.ReadToEnd(); writer.Write(s); } } } } }
Classes BinaryReader
and BinaryWriter
allow you to work with binary streams. There are
also so-called memory streams, StringReader
and StringWriter
, which allow you to use strings
as input and output streams.
2.6.2 Working with Binary Files
.NET tools include classes to conveniently work with binaries. The BinaryWriter
class provides functions
for writing to a data file various built-in value types, as well as byte arrays and character arrays. The BinaryReader
class
provides methods for reading data of all these types from a binary file. In order to create the corresponding streams,
we first create file streams (objects of FileStream
class). If fileName
is a string containing
a file name, streams are created as follows:
FileStream outputStream = new(fileName, FileMode.Create); FileStream inputStream = new(fileName, FileMode.Open);
Further, these streams are used to create objects of types BinaryWriter
and BinaryReader
classes:
BinaryWriter writer = new BinaryWriter(outputStream); BinaryReader reader = new BinaryReader(inputStream);
There are a number of Write()
methods of the BinaryWriter
class designed to record data
of various types. The object of BinaryReader
class can read this data using the methods ReadInt16()
, ReadInt32()
, ReadInt64()
, ReadDouble()
, ReadDecimal()
etc.
The best way of writing strings is to convert the string into an array of characters. Before writing this array
to the file, it is advisable to write its length. The storing of the string called s
will look like
this:
writer.Write(s.Length); writer.Write(s.ToCharArray());
When reading from a file, you should first read the length and then the characters from the array that was saved:
int len = reader.ReadInt32(); s = new String(reader.ReadChars(len));
Custom types can be stored, for example, as strings. If you need to store the date and time (an object of the DateTime
class),
the best way is to get the representation in the form of a long integer using ToBinary()
method. Then
the date and time can be reproduced in the corresponding object by reading the number of long
type
from the file long and using the static DateTime.FromBinary()
method.
In the following example, data of various types contained in an object of type Employee
is recorded:
namespace BinaryFilesDemo { public class Employee { public int Id { get; set; } public string Name { get; set; } = ""; public string Surname { get; set; } = ""; public DateTime DateOfBirth { get; set; } public decimal Salary { get; set; } public override string ToString() { string s = "Id:\t\t" + Id + "\nName:\t\t" + Name + "\nSurname:\t" + Surname + "\nDate of Birth\t" + DateOfBirth.ToShortDateString() + "\nSalary\t\t" + Salary; return s; } public void WriteToFile(string fileName) { using (FileStream fs = new(fileName, FileMode.Create)) { using (BinaryWriter writer = new(fs)) { writer.Write(Id); writer.Write(Name.Length); writer.Write(Name.ToCharArray()); writer.Write(Surname.Length); writer.Write(Surname.ToCharArray()); writer.Write(DateOfBirth.ToBinary()); writer.Write(Salary); } } } public void ReadFromFile(string fileName) { using (FileStream fs = new(fileName, FileMode.Open)) { using (BinaryReader reader = new(fs)) { Id = reader.ReadInt32(); int len = reader.ReadInt32(); Name = new String(reader.ReadChars(len)); len = reader.ReadInt32(); Surname = new String(reader.ReadChars(len)); DateOfBirth = DateTime.FromBinary(reader.ReadInt64()); Salary = reader.ReadDecimal(); } } } } class Program { static void Main(string[] args) { Employee employee = new() { Id = 1, Name = "John", Surname = "Smith", DateOfBirth = DateTime.Parse("1989/12/31"), Salary = 1000 }; Console.WriteLine(employee); employee.WriteToFile("employee.bin"); employee = new(); Console.WriteLine(employee); employee.ReadFromFile("employee.bin"); Console.WriteLine(employee); } } }
The program first creates an object of type Employee
. Then we display the data of the created object
and write it to a binary file. Next, we create a new empty object of Employee
type and read data from
the binary file. The object with the read data is displayed on the screen.
It should be remembered that working with binary files does not involve viewing and editing files outside a special program that was created for reading and writing data.
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 Publications
Suppose we want to expand previously created a program that describes the bookshelf, adding a hierarchy of publications
that can be located on the shelf. For example, separate types of publications are books and magazines.
Instead of an overloaded string casting operation, we'll implement an override of the ToString()
method.
In addition, methods for adding and removing publications and authors from arrays should be provided. We can also implement the functions of saving data in a text file and loading data from a text file.
We add classes Publication
, Magazine
, FileData
and FileUtils
to
the file FileUtils . In addition, we'll make the necessary changes to the classes that were created earlier. We
will get the following file code of Books.cs
file:
using System.Text; /// <summary> /// A namespace that contains the classes to represent the bookshelf /// </summary> namespace Bookshelf { /// <summary> /// Represents the author of the book on the bookshelf /// </summary> public class Author { public string Name { get; set; } = ""; 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> /// Represents a separate publication (book, magazine, etc.) /// </summary> public abstract class Publication { public string Title { get; set; } = ""; 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> /// 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 = Authors.Append(new Author(name, surname)).ToArray(); } /// <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 = Authors.Where(author => author.Name != name || author.Surname != surname).ToArray(); } } /// <summary> /// Represents a magazine on the shelf /// </summary> public class Magazine : Publication { public int Volume { get; set; } 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> /// Bookshelf /// </summary> public class Bookshelf { /// <summary> /// Array of references to publications /// </summary> public Publication[] Publications { get; set; } = { }; /// <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> /// <param name="publications">open array of publications</param> public Bookshelf(params Publication[] publications) { Publications = 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 = Publications.Append(publication).ToArray(); } /// <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 = Publications.Where(publication => publication.Title != title).ToArray(); } /// <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); } } /// <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.Length; i++) { result += string.Format(" {0}", book.Authors[i]); result += (i < book.Authors.Length - 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(); } } /// <summary> /// Provides methods for finding and sorting publications on a shelf /// </summary> public static class BookHandle { /// <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 Publication[] ContainsCharacters(Bookshelf bookshelf, string characters) { return Array.FindAll( bookshelf.Publications, 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) { Array.Sort(bookshelf.Publications, (b1, b2) => string.Compare(b1.Title.ToUpper(), b2.Title.ToUpper())); } } /// <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 add a new function called AdditionalProcessing()
to the Program.cs file.
We will get the following code:
namespace Bookshelf; /// <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 = BookHandle.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:"); BookHandle.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">fileName</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) { Console.WriteLine("Error reading from file " + fileName); 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"); } }
4 Exercises
- Create a class hierarchy "Book" and the "Handbook". Implement constructors and access
methods. Override
ToString()
method. In theMain()
function create an array that contains elements of different types. Display elements on the screen. - Create a class hierarchy "Movie" and "Serial". Implement constructors and access
methods. Override
ToString()
method. In theMain()
function create an array that contains elements of different types. Display elements on the screen. - Create a class hierarchy of classes "City" and "Capital". Implement constructors and
access methods. Override
ToString()
method. In theMain()
function create an array that contains elements of different types. Display elements on the screen. - Create a hierarchy of classes "Pet" and "Cat". Override
ToString()
method. In theMain()
function create an array that contains items of different types. Display items on the screen. - Create a class hierarchy "Planet" and "Satellite". Override
ToString()
method. In theMain()
function create an array that contains elements of different types. Display elements on the screen. - Read all lines from a text file to a list, and write these lines into another text file in reverse order.
- Read all lines of some text file to a list, and write even-numbered lines to another text file.
5 Quiz
- What is the purpose of inheritance?
- What is the difference between single and multiple inheritance?
- What elements of the base class are not inherited?
- How do you initialize the base class?
- Where and what you can use
base
keyword? - How to override method with
sealed
modifier? - Can you implicitly cast base class reference to a derived class reference?
- What are the opportunities of polymorphism?
- What is the difference between virtual and non-virtual methods?
- How to define virtual methods?
- What is the compiler reaction on the lack of
override
ornew
keywords before the overridden virtual method? - How to describe abstract methods and classes? Can abstract classes contain non-abstract methods?
- What are the advantages of interfaces versus abstract classes?
- How to define and implement an interface?
- What is the purpose of explicit interface implementation?
- What are the features of methods that explicitly implement interfaces?
- How to define and call an interface method with a default implementation?
- What is the idea behind the pattern matching mechanism?
- What is the purpose of exceptions mechanism?
- How to create an exception object?
- How to get the message concerned with some exception in C#?
- How to get stack trace in C#?
- Is it possible to call a function that throws an exception, without checking this exception? What will be the reaction of the system?
- How to work with text files in .NET applications?