CODEX

Intermediate C# [OOP & Structs, delegates]

In C#, we can find some useful OOP tools to make our code better.

Gilad Bar Ilan
CodeX
Published in
7 min readFeb 19, 2021

--

In this article, you will find some intermediate tools that you can use in your program.

NOTE — The operations (operator overloading, indexers) would work for structs as well. However, the examples would use classes.

Indexers

If you have ever used List<T> or Dictionary<T> and you were curious about how can an instance of a class can have indices just like an array, then the answer for this is indexers.

In the example below, we can see how to create an indexer.

T — Stands for the value we return from the index.

[int index] — The index the user gave as an argument.

public class SampleClass<T>
{
protected T[] arr;
public SampleClass(params T[] arr2)
{
this.arr = arr2;
}
public T this[int index]
{
get { return arr[index]; }
set { this.arr[index] = value; }
}
}

Note that we can make an indexer that get & return any type we want, if we had implemented a Dictionary<string, string> for example, we would replace int index with string key.

Main function:

public static void Main(string[] args)
{
SampleClass<int> sample = new SampleClass<int>(5,4,3);
sample[0] = 10; //now the array value is [10,4,3]
Console.WriteLine(sample[0]); //prints 10 to the console
}

In Visual Studio, you can write indexer and then tab twice to auto-completion of indexer structure.

Operator Overloading

If you’ve ever considered how to make an instance that would be able to use operators like ++, ==, then here is the answer.

public class Circle
{
public double radius { get; set; }
public const double PI = Math.PI;
public Circle(double Radius)
{
this.radius = Radius;
}

public static Circle operator ++(Circle myCircle)
{
myCircle.radius++;
return myCircle;
}
}
public static void Main(string[] args)
{
Circle instance = new Circle(45.45);
instance++;
Console.WriteLine(instance.radius);
}

The syntax for operator overloading is “public static (Type) operator (The operator we want to overload) (Arguments)”.

When we talk about operator overloading, the first argument is the instance.

In the operator ++ the only interaction of instance is with the instance itself, so that’s why the parameters in the ++ overload would be the caller only.

Because ++ for example is an assignment operator, the type the operator returns should be the same as the class that called that operator, in our case, Circle.

Your compiler would raise a compile-time error for returning other value than your instance’s class on ++.

instance = instance + 1; //raise error if it's not a Circle type

However, what about == for example, because this operation should return a boolean value, well in that case the value is not stored as the instance value.

That’s why we can make the operator overload to return a bool (because the value is not stored in the class called that operator we can make the == to return any type we want, however, the best practice would be to return a boolean).

public static bool operator ==(Circle myCircle, Circle myCircle2)
{
if (myCircle.radius == myCircle2.radius)
return true;
return false;
}
public static bool operator !=(Circle myCircle, Circle myCircle2)
{
if (myCircle.radius != myCircle2.radius)
return true;
return false;
}

In the example above, we compare the instances radiuses and return true or false if the condition is met.

Note that if you overload the == operator, you must implement the != operator.

The == and the != needs to be matches by what they return, for example we can’t say == return bool wherever != return double.

public static void Main(string[] args)
{
Circle first = new Circle(50);
Circle second = new Circle(60);
Console.WriteLine(first == second); //prints false
}

The operator overloading has some limitations, for example, we cannot overload all kinds of operators.

The list is taken from https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading

Extensions

An extension is our way to add methods for existed classes.

If you have ever wanted to add to the string type a function called PrintMe that prints the string value to the screen this is the optimal way to do that.

Why Not Inherit To Other Class?

A question that can be asked is why not to take the class Random for example and make CustomRandom class that will inherit from Random.

Well, the answer for that is we can't, not all classes can inherit to other classes because they marked as sealed classes, this means that other classes cannot implement them in inheritance which means that our only way to make our custom function to this class would be using extensions.

How do we make an extension?

For making extensions we need to follow some rules, first, an extended function should be nested inside a static class.

the method should follow the following rules.

public static class SampleExtentions
{
public static (Return Type) SampleFunction(this (Type Name) name)
{
//implementation
}
}

In the example above, “this (Type Name)” refers to the type we want to add the function for.

public static class StringExtention
{
public static void PrintMe(this string value)
{
Console.WriteLine(value);
}
}

In the example above, we made an extension called PrintMe to the string type.

In the example below, we use that method on the main function.

public static void Main(string[] args)
{
string myName = "Hello World!";
myName.PrintMe();
}

NOTE — The type in the extended methods can be also a Value Type.

Delegate

What is a delegate?

In the bottom line, the delegate is a function pointer. It means that the delegate stores the address of functions as the value.

The delegate can chain other addresses to his value as well, however, all the functions should take the same arguments and return the same value.

We can execute by saying “delegate_name(arguments)” all the functions (because they take the same arguments) and return the last function added return value.

To create a delegate we need to write:

public delegate int SampleName(int value); //declare the delegatepublic static void Main(string[] args)
{
SampleName delegate1 = Function1; //get the address of the function
delegate1 += (int value2) => return (value2 + 3); //chained them

int val = delegate1(5); //stores 8 because of the last function chained
delegate1 += Function1; //by that we execute the same function twice
}
public static int Function1(int value)
{
Console.Write("Hi!");
return value + 1;
}

Func<T> & Action<T> & Predicate<T>

The problem with normal delegate

The normal delegate has various problems. For example, if we have two delegates that take the same arguments and has the same return type but the delegate itself has a different name we won’t be able to chain them or equalize them.

public delegate void FirstDelegate();
public delegate void SecondDelegate();
public static void Main(string[] args)
{
FirstDelegate fi = Console.WriteLine;
SecondDelegate se = Console.WriteLine;
Func1(fi); //OK
Func2(se); //raise error because delegates not from same type
}
public static void CallDelegate(FirstDelegate fi)
{
fi(); //execute the delegate
}

Because of this problem, the user can define over and over the same delegate with the same arguments with the same return type for no reason.

The solution

Well, not any problem has a solution, but delegates have.

C# provides two other predefined classes named Func and Action which are kind of delegates.

Action — in actions, we simply write Action<arguments types> and the action will get the arguments as parameters types but the return value would be void.

  • Example to Action
Action<int> act = (int value) => Console.WriteLine(value);//pass a bigger function
act += (int value) =>
{
Console.WriteLine(value);
Console.WriteLine(value);
};

In the example above, we chained to the delegate a function that gets an integer and returns void.

  • What is Func

In Func the things are just like in Action, the only difference is the fact that the last generic type would be the return type.

Func<int, string> fun_ = (int value) =>
{
return value + " Hello";
};
string b = fun_(5); //now b stores "5 Hello"
fun_ += (int value) =>
{
return value + " World";
};
b = fun_(5); //now b stores "5 World"
  • What is Predicate

The predicate is a lot like Action, the main difference between them is the fact that Predicate always returns bool instead of void.

public static void Main(string[] args)
{
Predicate<int> pre = (int value) => value == 5;
Print(pre, 1,2,3,4,5,6); //prints 5 as the number of times it shown in the params
}
public static void Print(Predicate<int> pre, params int[] arr)
{
foreach(int item in arr)
{
if(pre(item)) //check the condition in the predicate
{
Console.WriteLine(item);
}
}
}

How do they help?

Func & Action & Predicate used globally, so it’s a good practice to use them, because everyone uses them we save the problem of having the same delegate with a different name.

However, consider the fact that sometimes it’s good to create our own delegates because Func & Action & Predicate make it hard to understand what is their purpose and we can’t give them a good explanation name class because they already defined.

Congratulations! You’ve finished the article! Hope you enjoyed it.

--

--