Skip to content

02 - OOP (2)

Nullable types

Sometimes value types need special state – value not known.

1
boolean SwitchIsOn = false;

If state of switch is not observed yet, both true and false values are unsuitable. For such cases, declare variable as nullable. Add ? to the type declaration.

1
boolean? SwitchIsOn = null;

Example of operations with nullable type (int? x):

  • if ( x.HasValue)
  • if (x != null)
  • int y = x ?? 0;
  • (?? - null-coalescing operator) - If value before ?? is not null, use it. Otherwise take value after ??.
  • Pattern matching with someVariable is type newVariableName.
1
2
3
4
5
6
if (x is int valueOfX) {
    Console.WriteLine($"x is {valueOfX}"); }
else
{
    Console.WriteLine("x does not have a value");
}

?: ?. ?[ ] ??

  • ? : - conditional operator (ternary conditional operator)
    • classify = (input > 0) ? "positive" : "negative";
  • ?. and ?[] - null-conditional Operators
    • int? length = customers?.Length;
    • Customer first = customers?[0];
    • int? count = customers?[0]?.Orders?.Count();
  • ?? - null-coalescing operator
    • int count = customers?[0]?.Orders?.Count() ?? 0;

Struct

Like a class, but struct is value type. Class is reference type.

Important qualities:

  • Can’t be inherited or used as a base class.
  • Can implement an interface(s)
  • Can’t have default constructor
  • Fields cannot be initialized unless they are declared as const or static.
  • Can be instantiated without the new keyword
  • Fields will remain unassigned and the object cannot be used until all of the fields are initialized. This includes the inability to get or set values through auto-implemented properties.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public struct CoOrds{
    public int x, y;
    public CoOrds(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

static void Main()
{
    // Initialize:
    CoOrds coords1 = new CoOrds();
    CoOrds coords2 = new CoOrds(10, 10);
    // Declare an object:
    CoOrds coords3;
    // Initialize:
    coords3.x = 10;
    coords3.y = 20;
}

When to use Struct?

CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects. AVOID defining a struct unless the type has all of the following characteristics:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

Record C# 9.0

Reference type that provides functionality to capsulate data.
While records can be mutable, they are primarily intended for supporting immutable data models.

  • Concise syntax for creating a reference type with immutable properties
  • Built-in behavior useful for a data-centric reference type:
    • Value equality
    • Concise syntax for nondestructive mutation
    • Built-in formatting for display
  • Support for inheritance hierarchies (from record to record)

Immutable record

1
public record Person(string FirstName, string LastName);
1
2
3
4
5
public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
};

Mutable record

1
2
3
4
5
public record Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
};

Records are distinct from classes in that record types use value-based equality. Two variables of a record type are equal if the record type definitions are identical, and if for every field, the values in both records are equal.

C# 10 added record struct to define records as value types. Default is record class.

Boxing

Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type.

Boxing is implicit; unboxing is explicit.

Boxing is expensive!

1
2
3
4
5
6
int i = 123;
// The following line boxes i.
object o = i;

o = 123;
i = (int)o;  // unboxing

Tuples

Lightweight, unnamed value types that contain multiple public fields.

1
2
var letters = ("a", "b");
Console.WriteLine(letters.Item1);
1
var alphabetStart = (Alpha: "a", Beta: "b");
1
(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");

Warning

For the last example, left side wins!

Tuples are most useful as return types for private and internal methods. Tuples provide a simple syntax for those methods to return multiple discrete values: You save the work of authoring a class or a struct that defines the type returned. There is no need for creating a new type.

Deconstructing the tuple:

1
(int max, int min) = Range(numbers); // range returns tuple

Discarding tuple elements:

Discard is a write-only variable whose name is _ (the underscore character). You can assign all of the values that you intend to discard to the single variable.

1
var (_, _, pop1, _, pop2) = QueryCityDataForYears("New York", 1960, 2010);

Delegates

A delegate is a type that defines a method signature, and can provide a reference to any method with a compatible signature.

You can invoke (or call) the method through the delegate.

Delegates are used to pass methods as arguments to other methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public delegate void SampleDelegate(string str);
class SampleClassDelegate
{
    // Method that matches the SampleDelegate signature.
    public static void SampleMethod(string message)
    {
        // Add code here.
   }
    // Method that instantiates the delegate.
    void SampleDelegate()
    {
        SampleDelegate sd = SampleMethod;
        sd("Sample string");
    }
}

Func<T1,..T16, OutT>, Action<T1,..T16> and Predicate<T> delegates

Three predefined universal (generic) delegates. Action returns void, Predicate returns bool and Func returns user defined type. Number of input paramaters: 0 to 16.

1
2
3
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
1
2
3
4
Func<string, string> superFunc1 = (str) => str.ToUpper();
Func<int, int, string> superFunc2 = (x,y) => (x+y).ToString().ToUpper(); 
var someStr1 = superFunc1("FooBar"); // FOOBAR
var someStr2 = superFunc2(1, 2); // 3
1
2
3
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
1
2
Action<string> outputFunc = (str) => Console.WriteLine(str); 
outputFunc("Fido");

Parameter type T – can use up to 16 parameters.

1
public delegate bool Predicate<in T>(T obj);

Array and Collections

For many applications, you want to create and manage groups of related objects. There are two ways to group objects:

  • by creating arrays of objects
  • by creating collections of objects.

Arrays are most useful for creating and working with a fixed number of strongly-typed objects.

Collections provide a more flexible way to work with groups of objects. Unlike arrays, the group of objects you work with can grow and shrink dynamically as the needs of the application change.

1
2
3
4
5
6
7
              ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
              │      │ │      │ │      │ │      │
Data ────────►│  10  │ │  20  │ │  30  │ │  40  │
              │      │ │      │ │      │ │      │
              └──────┘ └──────┘ └──────┘ └──────┘

Index  ──────►   0        1        2        3

All array (and collection) types are implicitly derived from System.Array, which itself is derived from System.Object. This means that all arrays are always reference types which are allocated on the managed heap, and your app's variable contains a reference to the array and not the array itself.

Arrays

Array – when working with fixed number of strongly typed objects.
int[] intArray = new int[5];

Arrays cannot be resized. You have to create new copy - Array.Resize
public static void Resize<T> (ref T[] array, int newSize);

Arrays can be multidimensional.
int[,, ] intarray = new int[4, 2, 3];

Array can contain arrays (Jagged Arrays).

1
2
3
4
5
int[][, ] arr = new int[3][, ] {
    new int[, ] {{1, 3}, {5, 7}},
    new int[, ] {{0, 2}, {4, 6}, {8,10}},
    new int[, ] {{11, 22}, {99, 88}, {0, 9}}
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void Main(string[] args)
{
    Console.WriteLine("Hello Array!");
    int[,] x = {
        { 1, 2, 3 },
        { 4, 5, 6 },
    };
    // 6 2 2 3
    Console.WriteLine($"{x.Length} {x.Rank} {x.GetLength(0)} {x.GetLength(1)}");
}

Properties

Rank – how many dimensions
Length – total number of elements in array (over all dimensions)

Methods

void Clear (Array array, int index, int length) – set elements to default values
object Clone() – create shallow copy
void Copy(...) – copy array elements to another array
bool Exists<T> (T[] array, Predicate<T> match)
public delegate bool Predicate<in T>(T obj);
T Find<T> (T[] array, Predicate<T> match);
FindIndex<T>(T[], Predicate<T>)
ForEach<T> (T[] array, Action<T> action);
int GetUpperBound (int dimension);
Sort(Array, Int32, Int32, IComparer), Sort(Array), ....

Collections and Arrays

All array (and collection) types are implicitly derived from System.Array, which itself is derived from System.Object. This means that all arrays are always reference types which are allocated on the managed heap, and your app's variable contains a reference to the array and not the array itself

Collections

Collections grow and shrink dynamically and provide lots of functionality. 90%++ of time you need one of these 5 essential collection types, however there are lots more of special collections for cases when speed/concurrency are critical.

The five essential ones are:

  • List (Estonian – nimekiri)
  • Dictionary (Estonian - sõnastik)
  • HashSet (Estonian – paisktabel)
  • Stack (Estonian – magasin ehk pinu)
  • Queue (Estonian – järjekord ehk saba)

List

Represents a list of objects that can be accessed by an index.

Unlike arrays that are fixed in size, lists can grow in size dynamically.

Internally, a list uses an array for storage. If it becomes full, it’ll create a new larger array, and will copy items from the existing array into the new one.

1
2
var list = new List<int>();
var list = new List<int>(10000);

You can set an initial size to list to avoid cost of resizing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Add an item at the end of the list
list.Add(4);

// Add an item at index 0
list.Insert(4, 0);

// Remove an item from list
list.Remove(1);

// Remove the item at index 0
list.RemoveAt(0);

// Return the item at index 0
var first = list[0];

//Return the index of an item
var index = list.IndexOf(4);

// Check to see if the list contains an item 
var contains = list.Contains(4);

// Return the number of items in the list 

var count = list.Count;

// Iterate over all objects in a list 
foreach (var item in list)
    Console.WriteLine(item);

Dictionary

Dictionary is a collection type that is useful when you need fast lookups by keys (keys are unique).

To create a dictionary, first you need to specify the type of keys and values.

1
var dictionary = new Dictionary<int, Customer>();

A dictionary internally stores objects in an array, but unlike a list, where objects are added at the end of the array (or at the provided index), the index is calculated using a hash function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dictionary.Add(customer.Id, customer);

var dictionary = new Dictionary<int, Customer>{ {customer1.Id, customer1}, ...};

// Return the customer with ID 1234
var customer = dictionary[1234]; 

// Removing an object by its key
dictionary.Remove(1);

// Removing all objects
dictionary.Clear();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var count = dictionary.Count;
var containsKey = dictionary.ContainsKey(1);
var containsValue = dictionary.ContainsValue(customer1);

// Iterate over keys
foreach (var key in dictionary.Keys) Console.WriteLine(dictionary[key]);

// Iterate over values
foreach (var value in dictionary.Values) Console.WriteLine(value);

// Iterate over dictionary
foreach (var keyValuePair in dictionary) {
    Console.WriteLine(keyValuePair.Key);
    Console.WriteLine(keyValuePair.Value);
}

HashSet

A HashSet represents a set of unique items, just like a mathematical set (e.g. { 1, 2, 3 } == {3, 2, 1}).

Use a HashSet when you need super fast lookups against a unique list of items.

A HashSet, similar to a Dictionary, is a hash-based collection, so look ups are very fast with O(1). But unlike a dictionary, it doesn’t store key/value pairs; it only stores values.

So, every objects should be unique and this is determined by the value returned from the GetHashCode method.

So, if you’re going to store custom types in a set, you need to override GetHashCode and Equals methods in your type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var hashSet = new HashSet<int>();

// Initialize the set using object initialization syntax 
var hashSet = new HashSet<int>() { 1, 2, 3 };

// Add an object to the set
hashSet.Add(4);

// Remove an object
hashSet.Remove(3);

// Remove all objects
hashSet.Clear();

// Check to see if the set contains an object
var contains = hashSet.Contains(1);

// Return the number of objects in the set
var count = hashSet.Count;

HashSet provides many mathematical set operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Modify the set to include only the objects present in the set 
// and the other set
hashSet.IntersectWith(another);

// Remove all objects in "another" set from "hashSet"
hashSet.ExceptWith(another);

// Modify the set to include all objects included in itself, 
// in "another" set, or both
hashSet.UnionWith(another);
var isSupersetOf = hashSet.IsSupersetOf(another);
var isSubsetOf = hashSet.IsSubsetOf(another);
var equals = hashSet.SetEquals(another);

Stack

Stack is a collection type with Last-In-First-Out (LIFO) behaviour.

We often use stacks in scenarios where we need to provide the user with a way to go back.

Internally, a stack is implemented using an array. Since arrays in C# have a fixed size, as you push items into a stack, it may need to increase its capacity by re-allocating a larger array and copying existing items into the new array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var stack = new Stack<string>(); 

// Push items in a stack
stack.Push("http://www.google.com");

// Check to see if the stack contains a given item
var contains = stack.Contains("http://www.google.com"); 

// Remove and return the item on the top of the stack
var top = stack.Pop();

// Return the item on the top of the stack without removing it
var top = stack.Peek();

// Get the number of items in stack
var count = stack.Count;

// Remove all items from stack
stack.Clear();

Queue

Queue represents a collection with First-In-First-Out (FIFO) behaviour. We use queues in situations where we need to process items as they arrive.

Three main operations on queue include:

  • Enqueue: adding an element to the end of a queue
  • Dequeue: removing the element at the front of the queue
  • Peek: inspecting the element at the front without removing it.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var queue = new Queue<string>(); 

// Add an item to the queue
queue.Enqueue("transaction1");

// Check to see if the queue contains a given item
var contains = queue.Contains("transaction1");

// Remove and return the item on the front of the queue
var front = queue.Dequeue();

// Return the item on the front without removing it
var top = queue.Peek();

// Remove all items from queue
queue.Clear();

// Get the number of items in the queue
var count = queue.Count;

Collections summarized

Lists are fast when you need to access an element by index, but searching for an item in a list is slow since it requires a linear search.

Dictionaries provide fast lookups by key. Keys should be unique and cannot be null.

HashSets are useful when you need fast lookups to see if an element exists in a set or not.

Stacks provide LIFO (Last-In-First-Out) behaviour and are useful when you need to provide the user with a way to go back.

Queues provide FIFO (First-In-First-Out) behaviour and are useful to process items in the order arrived.