Creating Nested Classes and Generic Types

Creating Nested Classes and Generic Types

Nested Classes and Generic Types

In this tutorial, we will explore how to create nested and generic types. These two features of C# will improve reusable code and type safety.

Nested Types

When a new type is defined within another type such as an interface, struct, or class, we refer to this type as being a nested type.

Due to being defined within another structure, the accessibility of this type will default to private, this means it is only accessible from within the outer type. You can change the accessibility of the nested type using an access modifier such as public, protected, internal etc


using System;

public class Car
{
	public string Brand { get; set; } = "Generic";
	public Tyre FL { get; set; }
	public Tyre FR { get; set; }
	public Tyre RR { get; set; }
	public Tyre RL { get; set; }

	public Car()
	{
		FL = new Tyre("FL");
		FR = new Tyre("FR");
		RR = new Tyre("RR");
		RL = new Tyre("RL");
	}

	public class Tyre
	{
		public string position { get; set; }
		public int TyreSize { get; set; } = 18;

		public Tyre(string position)
		{
			this.position = position;
		}
	}
}

public class Program
{
	public static void Main()
	{
		Car car = new Car();
		Console.WriteLine(car.FL.position);
	}
}
                

Generic Types

Generics allow for code to be written in a type agnostic way, it reduces the amount of code we have to write and increases templating.

Imagine we were writing an application for a library. At some point we might need a collection that is intended to store custom objects for all the fiction books that are on loan, we can implement this type. But now imagine that we also need to do the same thing for all of the non-fiction books which are of a different type. Instead of having to write different types for fiction and non-fiction books, we can instead implement one generic type that can deal with both of these classes, thus writing less code and making fixes easier.

We saw how are able to use the object type hold every other C# type. However, the object type is not an optimal solution to templating as there are substantial overheads from boxing and unboxing.

You can write classes using the generic type parameter: T. You can use this wherever you would normally outline the type and identifier of a parameter.


using System;

public class GenericList <T\>
{

    public void Add(T input)
	{
		Console.WriteLine($"This is a {input.GetType()}");
	}
}

public class TestClass{}

public class Program
{
	public static void Main()
	{
		GenericList <int> newGenericList = new GenericList <int>();
		newGenericList.Add(5);

		GenericList <string> stringList = new GenericList <string>();
		stringList.Add("Hello");

		TestClass testClass = new TestClass();
		GenericList <TestClass> testList = new GenericList <TestClass>();
		testList.Add(testClass);


	}
}
                

As shown in the example, when you come to utilise a generic type, the target type should be included in angled brackets so that the runtime anticipates what kind of data to expect.

C# already has a number of Generics defined for general use, you can find these generics in the System.Collections.Generic namespace and contain popular types such as:

  • Dictionary<TKey, TValue>
  • List<T>
  • Queue<T>
  • Stack<T>
  • LinkedList<T>

The Generic Type Parameter T

The generic type parameter T can be used in a number of circumstances where you would otherwise outline the concrete type.

The generic type parameter can be used as:

  • A replacement for the concrete type in class signatures
  • In type constructors
  • Member fields (a name is required)
  • Member properties (a name is required)
  • Method parameters
Generic types and T
Type parameter T

Generic Methods

A generic method is a method that uses type parameters instead of explicit types. Generic methods are useful if you wish to only define one method to cope with a range of different data types. A generic method does not have to be declared within a generic type, it can be declared within normal classes etc.

Generic methods are defined in the following manner:

static void EchoValue<T>(T val1, T val2)
{
    Console.WriteLine($"Value one is {val1}");
    Console.WriteLine($"Value two is {val2}");
}

Notice the use of T, a generic type parameter.

The generic method can be called like so:

public static void Test()
{
    int a,b,c,d = 5;
    string e = "Hello";
    string f = "Hi";

    EchoValue<int>(a, b);
    EchoValue<string>(e, f);
}

If you do not include the type in angled brackets, the same result will occur as the compiler will infer the type. You may still want to include it for readability purposes.

Generics and the Compilation Process

Although we are writing types to support generic type parameters, it is important to remember that the compiler will close these types at compilation time, essentially making copies of the used generics with the actual data type in place.

It is possible to have an open generic at runtime but there is only one way to do this and that is to call the type of method on a class that has type parameters without specifying a type for the generic.

class Test<T>{}

Type test1 = typeof(Test<>);

Default Values for Generics

The default keyword can be used to get a default value for the generic type parameter. In C#, the default value for reference types is null and the default for a value type is the value represented by zeroing all of the bits.

Constraining Generics

Type parameters can be substituted with any type by default, this behaviour can be constrained so that only some types are allowed. To do this, we can use the ‘where’ keyword to limit the accepted types.

Generic type parameters can be limited by the following properties:

  • Accepting classes that are derived from a base class
  • Only accepting classes that implement a specific interface
  • Accepting certain structs
  • Only accepting reference types
  • Accepting non-null values
  • Only accepting types that have a parameterless constructor
class StoreData<T> where T: class
{
    public T Instance{ get; set; }
}

For example, the above constraint will mean that only a reference type will be compatible with the type parameter. Generic constraints can be used anywhere that a type parameter is used, whether this is in a type definition or defining a method.

More information on these constraints can be found at Microsoft’s documentation for C#.

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

Constraints help to signify the expectations of the generic and what data they have been designed to work with.

Generic Subclasses

A generic class can be inherited from just as any other regular class would be able to. In the inherited class, the type parameter is still left open and therefore can be used in the same manner.

Static Data and Generic Types

When using static member within generic types, each closed type will have a separate instance of the static data. For example, if a string is used in conjunction with a type parameter, the static data for that closed type will be independent of that of integers.