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; thethis
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
- Expand
int
type with method of calculating the square. - Expand
double
type with method of calculating the third power. - Expand
int
type with method of calculating factorial. - Expand
double
type with method of calculating integer power. - Expand
System.String
class with method of removal the first and the last characters. - Expand
System.String
class with method of check whether first and last letters are the same . - Create a class "Group of people". Implement operator overloading for adding (+) and removal (-) group members. Use object initializers.
- Create a Class "A simple fraction". Implement overloading for operations +, -, and *.
- 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. - Create a class "Group" with nested structure for the representation of student and an array of students.
- Create a class "Bookcase" with nested structure for the representation of books and an array of books.
- Create an enumeration for representing days of week. Print all values on the screen.
- Create an enumeration for representing months. Print all values on the screen.
5 Quiz
- What are the advantages and disadvantages of object initializers in comparison with constructors?
- Is it possible to initialize private fields and properties by using object initializers?
- How can you create a static class?
- What are advantages and disadvantages of static classes?
- What is operator overloading?
- How to describe an operator function?
- Is it possible to call operator function instead of using the operator?
- 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?
- What are differences in implementation of methods inherited from abstract classes in Java and C#?
- How to describe and implement an interface?
- Is it allowed multiple inheritance of interfaces?
- What is the difference between definition (usage) interfaces in Java and C#?
- 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 reason of using anonymous types?
- What are the features of creating and using records?
- What are the advantages and disadvantages of structures?
- How to describe the structure?
- How to define default constructor for the structure?
- What are the features of creating structure type objects using
new
operator? - What is structure boxing?
- How can we describe and use the enumeration?
- What is the difference between implementation of enumerations in Java and C#?
- Syntax and advantages of usage tuples.
- Can you add static methods to the previously created classes without modifying their source code?
- How to describe a method, which is added to an existing class?
- What is the difference between usage and implementation of nested classes in Java and C#?
- Is it possible to create nested class inside interface?