Introduction
When working with collections, such as lists of different types, it's essential to optimize your code for performance and maintainability. The conventional approach without the use of generics can lead to code duplication, increased maintenance effort, and reduced flexibility. In this post, we'll explore how generics can streamline your code, making it more efficient and adaptable.
The Issue with Non-Generic Code
Consider a scenario where we have lists of cars and motors, and we want to initialize these lists, write them to CSV files, and read from those files. Without generics, we would end up duplicating code, creating separate methods for cars and motors, leading to maintenance challenges as new types are introduced.
Non-Generic Code Example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GenericsVezba
{
internal class Program
{
static void Main(string[] args)
{
List<Car> cars = new List<Car>();
List<Motor> motors = new List<Motor>();
InitializeLists(cars, motors);
string fileCarsPath = @"C:\Tempo\cars.txt";
string fileMotorsPath = @"C:\Tempo\motors.txt";
WorkWithFile.WriteCarFile(fileCarsPath, cars);
var carsFromFile = WorkWithFile.ReadCarFile(fileCarsPath);
WorkWithFile.WriteMotorFile(fileMotorsPath, motors);
var motorsFromFile = WorkWithFile.ReadMotorFile(fileMotorsPath);
foreach (var car in carsFromFile)
{
Console.WriteLine($"{car.Title}, {car.Description}, {car.Number}");
}
Console.WriteLine();
foreach (var motor in motorsFromFile)
{
Console.WriteLine($"{motor.Title}, {motor.Description}, {motor.Number}");
}
}
public static void InitializeLists(List<Car> cars, List<Motor> motors)
{
cars.Add(new Car {Title="Ford", Description="Best Car Ever.", Number=1423 });
cars.Add(new Car { Title = "Mustang", Description = "Best Car in Serbia.", Number = 1213 });
cars.Add(new Car { Title = "Mercedes", Description = "Good fast and comfortable car.", Number = 5723 });
motors.Add(new Motor { Title = "BMW", Description = "Best Motor Ever.", Number = 1423 });
motors.Add(new Motor { Title = "Honda", Description = "Best Motor in Serbia.", Number = 1213 });
motors.Add(new Motor { Title = "Kawasaki", Description = "fast motor.", Number = 5723 });
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericsVezba
{
static class WorkWithFile
{
// ---
public static void WriteCarFile(string filePath, List<Car> cars)
{
List<string> lines = new List<string>();
lines.Add("Title, Description, Number");
foreach (var car in cars)
{
lines.Add($"{car.Title}, {car.Description}, {car.Number}");
}
System.IO.File.WriteAllLines(filePath, lines);
}
// ---
public static void WriteMotorFile(string filePath, List<Motor> motors)
{
List<string> lines = new List<string>();
lines.Add("Title, Description, Number");
foreach (var motor in motors)
{
lines.Add($"{motor.Title}, {motor.Description}, {motor.Number}");
}
System.IO.File.WriteAllLines(filePath, lines);
}
// ---
public static List<Car> ReadCarFile(string filePath)
{
var lines = System.IO.File.ReadAllLines(filePath).ToList();
lines.RemoveAt(0);
List<Car> cars = new List<Car>();
foreach (var line in lines)
{
var val = line.Split(',');
cars.Add(new Car() { Title = val[0], Description = val[1], Number = Convert.ToInt32(val[2]) });
}
return cars;
}
// ---
public static List<Motor> ReadMotorFile(string filePath)
{
var lines = System.IO.File.ReadAllLines(filePath).ToList();
lines.RemoveAt(0);
List<Motor> motors = new List<Motor>();
foreach (var line in lines)
{
var val = line.Split(',');
motors.Add(new Motor() { Title = val[0], Description = val[1], Number = Convert.ToInt32(val[2]) });
}
return motors;
}
}
}
As we delve deeper into the realm of code management, we realize that traditional approaches, as demonstrated, by the need for separate functions for each new class and file, can quickly lead us into a maintenance nightmare. But fear not, as a powerful solution emerges in the form of generics, a key tool in our arsenal.
The Power of Generics
Generics allow us to write more versatile and reusable code. Instead of writing separate methods for each type, we can create a single generic method that works with any type, as long as it meets certain criteria.
Benefits of Generics
Reusability: With generics, we only need one method to read and write lists of various types, reducing code duplication and maintenance effort.
Flexibility: We can easily extend our code to handle new types without rewriting existing methods, making our code more adaptable.
Performance: While generics can introduce slight overhead due to reflection, this is negligible compared to the performance gains from code reduction and increased maintainability.
Picture this: instead of crafting new functions for every class and file, we can employ generics to create a single implementation that dynamically adapts to different types. By using the industry-standard placeholder <T> within our class signatures, we open the doors to accommodating a diverse range of list types. However, a few essential conditions must be met for Generics to work.
public static List<T> ReadFunction<T>(string filePath) where T : class, new()
The code signifies that we're not just dealing with any class, but with classes that have an empty constructor and can be instantiated anew. But a question lingers: as we embrace this flexibility, how do we handle the variety of properties each class may have?
Reflection
Enter reflection, a double-edged sword. A powerful tool, yet one that demands our careful attention. With this technique, we can unravel the properties of any given class. But, a word of caution, excessive use of reflection can introduce performance penalties. Consider the following snippet:
// This could not be done if we didn't specify this:
// where T : class, new()
T entry = new T();
var cols = entry.GetType().GetProperties();
This code empowers us to navigate the intricate landscape of class properties.
Generic Code Example
Here's how we can leverage generics to improve our code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GenericsWithGenerics
{
internal class Program
{
static void Main(string[] args)
{
List<Car> cars = new List<Car>();
string filePath = @"C:\Tempo\cars.txt";
InitializeCarsList(cars);
GenericTextFile.WriteFunction(filePath, cars);
var carsFromFile = GenericTextFile.ReadFunction<Car>(filePath);
foreach (var car in carsFromFile)
{
Console.WriteLine($"{car.Title}");
}
}
public static void InitializeCarsList(List<Car> cars)
{
cars.Add(new Car { Title = "BMW", Description = "Best Car", Number = 3421 });
cars.Add(new Car { Title = "Mercedes", Description = "Best Car", Number = 9751 });
cars.Add(new Car { Title = "Audi", Description = "Best Car", Number = 9821 });
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;
namespace GenericsWithGenerics
{
public static class GenericTextFile
{
public static List<T> ReadFunction<T>(string filePath) where T : class, new()
{
List<T> list = new List<T>();
T entry = new T();
var cols = entry.GetType().GetProperties();
/* Try this commented code */
//Console.WriteLine(cols.Length);
//foreach ( var col in cols )
//{
//Console.WriteLine(col.Name);
//}
// Here, we have all lines
var lines = System.IO.File.ReadAllLines(filePath).ToList();
var header = lines[0].Split(',');
lines.RemoveAt(0);
foreach (var line in lines)
{
entry = new T();
var val = line.Split(',');
// Now I'm going through each line
for (var i = 0; i < header.Length; i++)
{
foreach (var col in cols)
{
// If the col.name and header match
if (header[i] == col.Name)
{
col.SetValue(entry, Convert.ChangeType(val[i], col.PropertyType));
}
}
}
list.Add(entry);
}
return list;
}
public static void WriteFunction<T>(string filePath, List<T> data) where T : class, new()
{
List<string> lines = new List<string>();
StringBuilder line = new StringBuilder();
T entry = new T();
var cols = entry.GetType().GetProperties();
foreach (var col in cols)
{
line.Append(col.Name);
line.Append(",");
}
lines.Add(line.ToString().Substring(0, line.Length - 1));
foreach (var item in data)
{
line = new StringBuilder();
foreach (var item1 in cols)
{
{
line.Append(item1.GetValue(item));
line.Append(",");
}
}
lines.Add(line.ToString().Substring(0, line.Length - 1));
}
System.IO.File.WriteAllLines(filePath, lines);
}
}
}
Feel free to copy and paste this code, and test its capabilities by integrating your own classes, each with their unique set of properties.
StringBuilder for Efficient String Handling
In our example, we use StringBuilder
to efficiently handle string manipulation, avoiding the memory overhead caused by immutable strings. StringBuilder
allows us to build strings with minimal memory allocation:
StringBuilder line = new StringBuilder();
T entry = new T();
var cols = entry.GetType().GetProperties();
foreach (var col in cols)
{
line.Append(col.Name);
line.Append(",");
}
Conclusion
Generics are a powerful feature in C# that enable more efficient, flexible, and maintainable code. By using a single generic method to handle various types, we can optimize our code and streamline our development process. The benefits of generics far outweigh any minor performance trade-offs, making them an essential tool for any C# developer.